SDK reference
The full @beeuni/sdk API surface — client methods, modules, turns, and distribution.
This page is the full @beeuni/sdk API surface: every client method, the
framework-free modules, the turns helpers, and how the package ships. Use
SDK to get started and Sessions for the
session lifecycle in depth.
The client
createBeeuni(config) returns one client. Every method is a typed call to
the platform API. The project(id) and session(pid, sid) handles bind ids
so you never repeat them.
const beeuni = createBeeuni({ backendUrl, getToken });
beeuni.accounts; // account / team operations
beeuni.accountInvites; // invite accept/decline by token alone
beeuni.projects; // top-level project operations
beeuni.connectors; // Connector calls scoped by an agent-minted token
beeuni.connector(slug); // one connector in the token's scope → see p.connector(slug)
beeuni.project(id); // id-bound project handle
beeuni.session(pid, sid); // id-bound session handle → see Sessions
beeuni.github; // GitHub App install + repo linking (account-scoped)
beeuni.gitBackend; // the instance git backend ("Beeuni managed")
beeuni.billing; // credits, subscription, tier, transactions
beeuni.sandboxShares; // public share links for a sandbox port
beeuni.connectStatus; // easy-connect status
beeuni.marketplace; // public marketplace catalog
beeuni.validateToken; // pasted-API-key check
beeuni.config; // platform config in effect
beeuni.runtime(); // runtime REST compatibility client
Accounts — beeuni.accounts
| method | what |
|---|---|
list() · get(accountId) |
accounts you belong to · one account |
create({ name }) · updateName(accountId, name) |
create · rename an account |
branding.get(accountId) · branding.update(accountId, { app_name }) · branding.uploadAsset(accountId, kind, file) · branding.removeAsset(accountId, kind) · branding.reset(accountId) |
organization branding (Enterprise): own logo / icon / favicon and product name; kind is logo · icon · favicon, or logo_dark · icon_dark · favicon_dark for the dark-scheme variant |
members(accountId) · invite(accountId, input) |
list members · invite one |
updateMemberRole(accountId, userId, role) · removeMember(accountId, userId) |
assign the account role · remove a member |
invites(accountId) |
pending invites |
cancelInvite(accountId, inviteId) · resendInvite(accountId, inviteId) |
cancel · resend a pending invite |
leave(accountId) |
leave the account |
accounts.tokens mints account-scoped API keys (beeuni_pat_...). See
SDK auth for the full token model.
| method | what |
|---|---|
tokens.list(accountId?, options?) |
list API keys — the whole account’s, or only your own with { mine: true } |
tokens.create(input) |
mint one — { accountId?, name, expiresAt?, projectId? } |
tokens.revoke(tokenId, accountId?) |
revoke one |
accounts.audit is the enterprise reconstruction log. It combines authenticated
API requests with semantic session, connector, approval, and computer events.
| method | what |
|---|---|
audit.log(accountId, filters?) |
list events by project, session, actor, source, outcome, request, correlation, resource, action, or time |
audit.export(accountId, filters?) |
export the same filtered event stream as CSV or JSONL |
audit.webhooks.list/create/update/remove(...) |
manage signed SIEM webhooks for the centralized stream |
Each event includes project_id, session_id, actor_type, source, outcome,
request_id, trace_id, and correlation_id when the action supplies them.
The API does not store request bodies, prompts, secrets, credentials, or raw
connector arguments in the centralized event. Connector events can include a
bounded argument preview that redacts credential-shaped fields and opaque data.
Access assignments
Beeuni has one grant record: an assignment. It binds one principal (user,
group, service_account, or pending) to one role, at one scope (account,
or one project), optionally narrowed to one object (agent, skill, secret,
app, or trigger) and optionally carrying an expires_at. Group access,
per-resource access, and custom-role bindings are all assignments. See
Accounts & access for the model.
The canonical REST surface is:
| Method + path | Does |
|---|---|
GET /v1/accounts/{accountId}/iam/assignments |
list assignments, filtered by principal, scope, object, or role |
POST /v1/accounts/{accountId}/iam/assignments |
create one assignment |
DELETE /v1/accounts/{accountId}/iam/assignments/{assignmentId} |
revoke one assignment |
GET /v1/accounts/{accountId}/iam/permissions |
the permission catalog, as data |
GET /v1/accounts/{accountId}/iam/roles · …/roles/{roleId}/permissions |
roles · one role’s permissions |
The SDK exposes them as listAssignments, createAssignment,
revokeAssignment, and listPermissions. A catalog row carries action,
scope_type, resource_type, delegable, description, area, level, and
implies — read it instead of hardcoding action strings.
Assigning a custom role needs the account’s rbac entitlement; the route answers
402 with code: "entitlement_required" without it.
Account invites — beeuni.accountInvites
Reached by invite token alone — the invitee may not be a member yet.
| method | what |
|---|---|
listMine() |
every pending, unexpired invite addressed to your email, with the workspace and any projects it grants |
describe(inviteId) · accept(inviteId) · decline(inviteId) |
preview · accept · decline an invite |
Projects — beeuni.projects
| method | what |
|---|---|
list() · listForAccount(accountId) |
your projects · projects in an account |
get(id) · detail(id) |
summary · full detail |
create(input) · createRepo(input) |
from an existing repo_url · new empty GitHub repo |
provision(input) |
new project on a new Beeuni-managed repo, seeded with a starter template — { name, account_id?, seed_starter?, starter_template?, marketplace_items?, source_item_id?, idempotency_key? } |
update(id, input) · archive(id) |
update settings · archive |
llmCatalog(id) · modelPicker(id) |
full · compact model catalog for a selector |
sandboxTemplates(id) · sandboxHealth(id) |
sandbox build templates · build health |
sessions(id) · createSession(id, input?) |
list visible sessions · create a session |
provision creates a new project; it does not start an existing project’s
sandbox. Start a session instead — see Sessions.
Send idempotency_key when a retry is possible — a reload, a second tab, a
timeout you retried. provision mints a brand-new managed repo per call, so
without a key those all create real duplicate projects. Reuse one key for every
attempt at a single logical create and the repeats return the project the first
attempt made (201, same project_id, push_token: null). The key identifies the
attempt, not the payload — reusing one with a different name returns the first
project and ignores the new value, so mint a fresh key per distinct create.
Creating a second project with the same name and no key still works.
A repeat that arrives while the first call is still provisioning gets 409 with
code: 'provision_in_flight' rather than a project_id that call may still roll
back. Retry with the same key.
project(id).sessions.list({ scope: 'project' }) is a lifecycle inventory for a
caller with project-manager permissions. It adds accessible unavailable, warm, and soft-deleted sessions with
ownership and runtime-state metadata. Both list scopes omit every session the
caller cannot open.
sessions.list and sessions.listPage also take the hierarchy and attribution
filters. Without them the list stays flat and unchanged.
| option | what |
|---|---|
parent: 'root' |
Top-level sessions only. Each row carries child_count. |
parent: '<sessionId>' |
That session’s children, newest first. |
startedBy: 'me' | 'others' | 'automated' |
Filter by initiator, relative to the caller. |
q |
Case-insensitive search over title, starter label, trigger slug, and session id prefix, across every session the caller may open. With parent: 'root', a root that matched through a child has search_match: 'child'. |
Every session row has parent_session_id and initiator
({ type: 'member' | 'trigger' | 'channel' | 'api' | 'system', id, label }).
initiator names who started the whole run; a child copies its parent’s.
sessionParentId(session) reads parent_session_id, then the legacy
metadata.spawned_by_session. A cursor is valid only for the filter set that
issued it.
GitHub — beeuni.github
Account-scoped GitHub App install and repo linking, not project-scoped.
| method | what |
|---|---|
getInstallation(accountId) · listInstallations(accountId) |
this account’s install · installs the user can reach |
saveInstallation(input) · deleteInstallation(accountId, installationId?) |
record · unlink an install |
listLinkableInstallations(input) |
installs the authorized GitHub user may link; each entry carries linked_to_other_accounts, a count of other Beeuni accounts holding it — never their names |
listRepositories(accountId, installationId?) · listRepositoryBranches(...) |
repos the install can see · branches and the GitHub default |
linkRepository(input) |
Import a repository as a project using an authorized GitHub installation. |
replaceProjectRepository({ project_id, repo_url, expected_repo_url, github_token }) |
replace an existing project’s GitHub repository. Use either github_token with Contents read/write access, or installation_id plus temporary github_user_token to verify organization admin access and grant only this repository through the GitHub App. copy_shared_secrets_from_project_id plus copy_shared_secret_identifiers copies selected shared runtime secrets in the same transaction. A stale expected_repo_url returns 409. The old repository remains on GitHub. New sessions use the replacement repository; existing sessions retain their checkout. |
This list holds real account connections only. “Beeuni managed” is not one of them — it is the instance backend below.
Instance git backend — beeuni.gitBackend
One deployment-wide owner plus credential, used for Beeuni-managed
repositories. It is never an account connection and never appears in
beeuni.github.listInstallations.
| method | what |
|---|---|
get() |
{ configured, kind: 'app' | 'pat' | null, owner } — readable by any authenticated user; it exposes only the owner login that every managed repo_url already carries |
Billing — beeuni.billing
Reads for credits, subscription, tier, and transaction history. Use Beeuni checkout and billing management to manage payments and credit purchases.
| method | what |
|---|---|
accountState(accountId?) · accountStateMinimal(accountId?) |
full · minimal billing state |
transactions(params?) · transactionsSummary(params?) |
history · summarized totals |
creditBreakdown(accountId?) · usageHistory(params?) |
credit balance by source · usage over time |
sessionCosts.list(options?) · sessionCosts.get(sessionId, options?) |
paginated session-cost records · one detailed session ledger |
tierConfigurations() |
available plan tiers |
checkout.createSession(input) · checkout.confirmSession(sessionId, accountId?) |
Deprecated. Reject with ENDPOINT_RETIRED. Removed in the next major. |
subscription.createPortalSession(...) · subscription.cancel(...) · subscription.reactivate(...) |
open the customer portal · cancel · reactivate |
subscription.cancelScheduledChange(...) · prorationPreview(...) |
cancel · preview a plan change |
subscription.scheduleDowngrade(...) |
Deprecated. Rejects with ENDPOINT_RETIRED. Change the plan in the customer portal. Removed in the next major. |
credits.purchase(input) · credits.autoTopupSettings(...) · credits.configureAutoTopup(...) |
one-off purchase · read · configure auto-topup |
sessionCosts.list() accepts accountId, projectId, limit, and offset.
Each row combines finalized LLM cost and billed sandbox compute cost.
sessionCosts.get() adds model usage and the discriminated LLM/compute ledger.
The list includes a reconciliation total for cost without a session.
Sandbox shares — beeuni.sandboxShares
Public share links for one exposed sandbox port. Sandbox-scoped, not project-scoped.
| method | what |
|---|---|
list(sandboxId) |
active share links |
create(input) |
create one — { sandboxId, port, ttl?, label? } |
revoke(sandboxId, token) |
revoke one |
Marketplace catalog — beeuni.marketplace
Public catalog browsing, read-only — distinct from project(id).marketplace,
which installs an item onto a project.
| method | what |
|---|---|
items(options?) · item(id) · itemFile(id, path) |
browse · one item · a file inside an item |
marketplaces() · featured() |
all · featured marketplaces |
sources.list() · sources.add(input) · sources.remove(id) |
list · add · remove a source |
Notifications — beeuni.notifications
The caller’s notifications, across every account and project. Each signed-in
person has one inbox, one preference record, and Web Push subscriptions for
their browsers. Every method needs a person’s credential: a service account
gets 403.
const page = await beeuni.notifications.list({ limit: 20 });
await beeuni.notifications.markRead({ ids: [page.notifications[0].id] });
await beeuni.notifications.updatePreferences({ kinds: { turn_done: { push: false } } });
| method | what |
|---|---|
list({ limit?, before? }, { signal? }) |
the newest page: { notifications, unread_count, next_before }. limit is 1 to 50 (default 20). Pass next_before as before for the next page. Rows the caller can no longer see are left out. A failure does not call the host error handler. |
markRead({ ids }) · markRead({ all: true }) · markRead({ sessionId }) |
mark 1 to 100 rows, every row, or every row of one session read. Changes only the caller’s rows. Returns { updated, unread_count }. |
preferences() · updatePreferences({ kinds }) |
read · change the push and email choice per kind. Returns { kinds, email_available }. A patch changes only the kinds and channels it names. |
webPushPublicKey() |
{ public_key }: the applicationServerKey for pushManager.subscribe |
registerWebPushSubscription({ endpoint, keys }) |
deliver push to this browser. Needs a browser sign-in: a personal token gets 403. An endpoint on an unknown push service gets 400. |
unregisterWebPushSubscription(endpoint, { signal? }) |
stop push to this browser. Returns { deleted }. |
registerDeviceToken(input) · unregisterDeviceToken(token, { signal? }) |
register · remove a native app’s Expo push token |
Kinds (InboxNotificationKind, listed in INBOX_NOTIFICATION_KINDS):
| kind | sent when | push | email (default) |
|---|---|---|---|
turn_done |
a turn you prompted or watch ended | on | off |
turn_error |
that turn ended with an error | on | on, in a digest |
question |
the agent asks a question | on | on, in a digest |
permission |
the agent asks for permission | on | off |
shared |
someone shared a session with you | on | on, in a digest |
automation_failed |
an automation you watch started to fail | on | on, at once |
automation_recovered |
that automation works again | on | off |
DEFAULT_NOTIFICATION_PREFERENCES holds these defaults. The inbox row is
always written; the preferences decide only push and email. A digest email
lists the rows still unread 15 minutes after they arrived, at most one digest
per hour. Hide the email column when email_available is false.
Each row’s url opens its subject. Its ?notification=<id> query lets the
page mark the row read.
The session handle carries the caller’s watch on one session:
| method | what |
|---|---|
session(pid, sid).watch() |
{ watching }: is the caller notified about this session |
session(pid, sid).setWatch(watching) |
false mutes the session for the caller, true watches it again. Prompting a session watches it, but never undoes a mute. |
session(pid, sid).presence({ tab_id, active, alerts? }, { keepalive? }) |
this tab’s presence lease. alerts: true says the tab shows its own notifications: with notification_center on, the server skips the phone and Web Push only while an alerting tab is in use. With it off, alerts is ignored and any present tab holds back the phone push. keepalive lets the request outlive a closing page. Default: on for an absent report, off for a present one. useSession sends keepalive only with presencePageExit. |
The project handle — beeuni.project(id)
Binds the project id; every sub-resource hangs off it.
const p = beeuni.project(projectId);
await p.detail();
await p.update({ name });
await p.llmCatalog();
| method | what |
|---|---|
get · detail · update · archive |
read · full detail · update · archive |
llmCatalog · modelPicker · sandboxHealth |
model and sandbox-build reads |
onboardingComplete |
mark project onboarding done |
validateManifest(raw) |
validate a beeuni.yaml (or legacy beeuni.toml) manifest server-side |
gitToken() |
mint a fresh scoped git push token (409 for a bring-your-own repo) |
setAgentScope(agentName, scope) |
set an agent’s allowed secrets and connectors in the manifest — the second binding, not a role |
p.tokens — project-scoped API keys
Auto-minted at session create as BEEUNI_TOKEN; can also be minted by hand.
| method | what |
|---|---|
list() |
project API keys |
create(input?) |
mint a new one |
revoke(tokenId) |
revoke one |
p.setupLinks — agent-minted setup links
A link a person opens to enter a secret or connect an app, without full project access.
| method | what |
|---|---|
requestSecret(input) · requestConnector(input) |
link to collect a secret · connect an app |
p.apps — Apps (releases, machines, volumes)
Behind the apps project feature. Guide: SDK → Apps.
| method | what |
|---|---|
list() · get(appId) · create(input) · update(appId, input) · remove(appId) |
inventory and lifecycle; create takes uses, update also takes labels |
deployments.* · artifacts.* · access.* · rollback · start · stop |
deployments, artifacts, the access policy, traffic and runtime |
machines.list/start/stop/restart/destroy · scale(appId, count) · exec(appId, input) |
machines per process group; exec needs project.app.admin |
volumes.list/create/update/destroy/snapshots/snapshot/restore/fork |
persistent volumes, daily snapshots, restore, fork |
token(appId) |
a 15-minute sign-in token; 409 app_capability_unsupported without member_tokens |
p.secrets — project secrets
| method | what |
|---|---|
list() · upsert(input) |
list metadata · create or update a write-only value and delivery policy |
setStrategy(identifier, strategy, options?) |
change the exposure and its host list |
broker(identifier, request) |
execute a session-authorized, policy-bound HTTPS request |
remove(identifier) |
delete a secret |
setPersonal(name, value) · removePersonal(name) |
set · remove a per-user override |
setGitCredential(input) |
set a git auth credential |
runtime with consumer sandbox is environment exposure — the default,
and the only policy that puts a plaintext value in the session. egress with
consumer network is egress-enforced exposure: the session holds a handle
and Beeuni substitutes the real value outside the sandbox, for the exact HTTPS
hosts the policy lists. Egress-enforced exposure is experimental; it needs the
secrets_egress feature flag (Settings → Feature flags). With the flag off,
setStrategy(identifier, 'egress', …) and upsert(...) with an egress policy
return 403 feature_disabled. Every broker consumer has no session presence
at all. See
Secrets. The broker(...) method requires a
session-scoped token and an active session handle.
p.access — project assignments, invites, requests
Every method here reads or writes an assignment scoped to this project. Project
roles are manager and member.
| method | what |
|---|---|
list() · invite(email, role) |
principals with access · invite a user |
update(userId, role) · revoke(userId) |
assign a project role · revoke the assignment |
pendingInvites() · requests() |
outstanding invites · pending access requests |
resendInvite(inviteId) · revokeInvite(inviteId) · approveRequest(id) · rejectRequest(id) |
resend/revoke an invite · approve/reject a request |
groupGrants() · attachGroupGrant(...) · updateGroupGrant(...) · detachGroupGrant(...) |
the same assignments, with a group principal |
p.access.resourceGrants is the object assignment view: it narrows a
principal to one object in the project instead of the whole project. Beeuni
enforces object assignments on agents and skills today. An agent is closed by
default — a member reaches it only when an assignment names them or one of their
groups. An object assignment carries no permissions of its own, and it restricts a
project manager as much as a member.
| method | what |
|---|---|
resourceGrants.list() |
object assignments in this project |
resourceGrants.create(input) |
assign one object to a user or a group |
resourceGrants.remove(grantId) |
revoke one object assignment |
p.connector(slug) — one connector
| method | what |
|---|---|
run(action, args, options?) |
the action’s output; throws ConnectorCallError or ConnectorApprovalPendingError — see Errors |
call(action, args, options?) |
the raw call result; 202 resolves with ok: false |
describe() · describe(action) |
the connector with input and output schemas · one action |
accounts() |
the accounts this caller may run it as, default first |
paginate(action, args, { next, maxPages? }) |
async iterator over each page’s output; throws ConnectorPageLimitError with nextArgs when more than maxPages (default 100) pages exist — see Pages |
uploadAttachment(content, input) |
p.connectors.uploadAttachment with connector set to this slug |
options: account, approvalContext, timeoutMs (default 90 000), signal. With the file from beeuni connectors types, args and output are typed.
p.connectors — tool connectors
| method | what |
|---|---|
catalog() · tools() |
callable Connector catalog · flattened <connector>.<action> tools |
search(query, options?) · describe(tool) |
find · inspect one callable tool |
call(tool, args?, options?) |
call one <connector>.<action> tool through the server-side gateway. options.account names which account to run as — see below. |
accounts(slug) |
the accounts this caller may call slug as, default first — what options.account matches against |
uploadAttachment(content, input) |
stage bytes for input.connector; returns ref ({ $beeuni_attachment }) to place in a later call’s args — see Attach files |
list() · config(connectorId) |
configured connectors · one connector’s config |
create(input) |
add a connector |
auth.discover(input) |
preview auth from an OpenAPI spec, Postman collection, or endpoint |
remove(connectorId) · sync() |
delete a connector · re-sync connectors |
setName(connectorId, name) · setSensitive(connectorId, sensitive) |
rename · mark it sensitive (extra approval gating) |
setAuthorizationStrategy(slug, strategy) |
@deprecated. Still calls the route; the route is a no-op that answers {ok:true, deprecated:true} and changes nothing. owner_type on each connection is the whole access rule now. |
setCredentialMode(connectorId, mode) · setCredential(connectorId, input) |
switch source · set the credential value |
policies.get(connectorId) · policies.set(connectorId, policies) |
read · replace its tool policies |
connections.list() · connections.reconcile(input) |
list · create/update connected accounts |
connections.updateCredential(connectionId, input) |
rotate a connection credential |
connections.revoke(connectionId) · connections.activate(connectionId) |
deny · restore a connection |
connections.setDefault(connectionId) |
make it the default for its owner scope |
connections.rename(connectionId, label) |
change the label only; the account stays authorized. Each connection carries connected_as, the identity it was authorized as. |
p.connectors.discover browses the direct-connector catalog. It is experimental and off by default — enable it per project under Settings → Feature flags → “Connectors API Discover”. Easy Connect remains the default connector marketplace.
| method | what |
|---|---|
discover.list(query?, cursor?) · discover.detail(id) |
search OpenAPI/MCP/GraphQL/CLI entries · one entry’s detail |
p.connectors.pipedream is the legacy managed-OAuth compatibility path; listApps
returns OAuth apps only. Use Easy Connect for new managed integrations. Connect API-key apps directly.
| method | what |
|---|---|
pipedream.listApps(params?) · pipedream.connect(input) · pipedream.finalize(input) |
browse the app catalog · start a connect flow · finalize it |
A connector defines the tool, provider app, and policies. connections
stores its connected accounts — a connector can hold several side by side.
A session can pin one for every call with
connector_bindings: { alias: { connection_id } }, or a single call can name
one with call(tool, args, { account }). Credentials stay encrypted and
resolve per request.
connections is the only active authorization facade. The retired
authorizations and profiles names are not part of the current SDK surface.
Accounts and the call-time gate
Each connection has an owner_type: project (shared — reachable by anyone
the connector is granted to, human or service account) or member (private —
reachable only by its own owner, and only in a private session; never by a
service account). A shared account can be narrowed to groups and people:
connections.list() returns its grants in shared_with, and
connectionSharedWithEveryone(connection) answers whether everyone in the
project may use it. A narrowed account follows the private rules for the
people it names, and usable: false marks one the caller sees only to manage
it. Grant or revoke with createAssignment / revokeAssignment
(object: { type: 'connection', id }) — see
Choose who can use a shared account.
p.connectors.accounts(slug) lists every account this caller may reach,
default first:
const accounts = await beeuni.project(projectId).connectors.accounts('stripe-read');
// [{ connection_id, label, owner_type, is_default }, ...]
await beeuni.project(projectId).connectors.call(
'stripe-read.get_balance',
{},
{ account: 'My Stripe' }, // a connection id, a label, or the words `me` / `project`
);
A call that names no account resolves to the caller’s own default private
account first, then the project’s default shared account. A named account
that does not resolve is denied with connector_not_connected — never
silently substituted — and the denial lists requested_account and
available_accounts. Every successful call echoes which account it ran as:
result.account = { connection_id, label, owner_type }.
There is no session-create-time connector gate any more. A session used to be
refused 409 CONNECTOR_CONNECTION_REQUIRED when a required connector had no
usable connection; that refusal is gone (require_connectors is inert — see
p.sessions). The gate moved to the
call: a denied connector_not_connected carries connect_url, a hosted
authorization link, whenever one can be minted for the caller — the whole
remedy is one click, surfaced by the transcript’s connect button.
p.policies — project policies
| method | what |
|---|---|
list() · set(policies) |
the project’s policies · replace the set |
p.triggers — cron, webhook, monitor and event automations
A trigger starts an agent action on a schedule, an inbound webhook, a monitored command, or an app event on a connector. See Triggers for session strategy and payload templating.
| method | what |
|---|---|
list() |
all triggers |
eventTypes({ connector }) |
the app events a connector can trigger on ({ provider, app, event_types }); 404 for an unknown connector, 409 event_source_unavailable when its provider has no event source |
create(input) |
create one |
update(triggerId, input) |
edit a trigger |
remove(triggerId) |
delete a trigger |
fire(triggerId) |
run it now |
setActivation(paused) |
pause or resume every trigger on the project |
create(input) takes { name, type: 'cron' | 'webhook' | 'monitor' | 'event', prompt_template, slug?, agent?, model?, enabled?, session_mode?, session_id?, cron?, run_at?, timezone?, secret_env?, connector?, event?, event_source?, event_account?, event_config?, session_access? }. name and prompt_template are required.
cron/run_at are mutually exclusive (type: 'cron'); secret_env (the
webhook HMAC secret) applies to type: 'webhook'.
An event trigger needs connector (a connector slug) and event (a type id
from eventTypes). event_account is optional: the label of one shared account of
that connector; null or omitted uses the connector’s default shared account.
event_source is optional: the event source adapter id, such as composio. null or omitted uses the
connector’s provider. The type id belongs to that adapter. event_config is shaped by that type’s config_schema.
eventTypes and eventApps report the adapter as source; provider is a deprecated alias.
Each listed trigger carries event: { connector, account, connected_as, type, config, source, provider, app, status: 'active' | 'needs_connection' | 'error' | 'pending', error, last_event_at } | null. Prompt templates read the event as
{{ event.data.<field> }} plus event.id, event.type, event.app,
event.connector, and event.occurred_at.
session_access controls who can open sessions the trigger creates. It is
{ mode: 'private' | 'members' | 'project', memberIds: string[], groupIds: string[] } and defaults to private. This policy is account-local runtime
state. It does not enter the portable beeuni.yaml manifest. Updating only
session_access creates no Git commit. A pinned session keeps its own sharing
settings. A project manager can always open trigger-created sessions, including
sessions that use private or selected-member access.
session_access is a per-resource visibility setting on top of the role model,
not a role. It decides who can open one trigger’s sessions. It grants no
permission the role verdict denies. See
Accounts & access.
p.marketplace / p.registry — installed items
Installs a catalog item’s files onto the project’s default branch.
registry.* is an identical alias of marketplace.*.
| method | what |
|---|---|
marketplace.list() · marketplace.install(id) |
installed items · install a catalog item |
marketplace.updates() · marketplace.update(name) · marketplace.updateAll() |
available updates · update one · update all |
marketplace.remove(name) |
uninstall an item |
p.files — repo files (read)
Read-only access to the project’s git tree. To read and write files inside a running session, use the session’s file operations — see Files under Modules below.
| method | what |
|---|---|
listDirectory({ ref?, path? }) |
one folder level: { entries: [{ path, type, size? }], truncated }. Use it to build a tree; every folder is complete up to 5,000 entries. |
list(options?) · read(path, ref?) |
every file under a path, recursively, cut at 1,000 (the response carries X-Beeuni-Truncated: 1 when cut) · a file’s contents at a git ref |
search(query, { content?, ref?, limit? }) |
search filenames over the whole repo, or contents with content: true |
archive(options?) · history(path) |
download a tarball · a file’s git history |
p.git — history
| method | what |
|---|---|
commits() |
the commit log |
commit(sha) · commitDiff(sha) |
one commit · its diff |
branches() · versionDiff(from, to) |
branches · diff between two refs |
p.changeRequests — lifecycle and merge
A change request (CR) is how a session’s work merges into the default branch.
| method | what |
|---|---|
list() · get(crId) |
open CRs · one CR |
diff(crId) · mergePreview(crId) |
its diff · preview the merge result |
open(input) · merge(crId, input?) |
open · merge a CR |
close(crId, input?) · reopen(crId, input?) |
close without merging · reopen a closed one |
requestChanges(crId, input) |
record feedback, optionally delivered back to the originating session |
p.sessions — and the session handle
| method | what |
|---|---|
list() |
the project’s sessions |
create(input?) |
create a session |
session(sid) |
the session handle (same as beeuni.session(id, sid)) |
The session handle is the heart of the runtime — see Sessions.
create(input) accepts connector_bindings keyed by connector-connection slug.
Each value names a connection_id. It also accepts secrets for
backend-origin secret narrowing. require_connectors is deprecated and
inert: the field is still accepted (an old client is never rejected for
sending it), but nothing reads it — a session is never refused at create time
for a connector with no usable account. See Accounts and the call-time
gate.
p.approvals — the connector approval inbox
Pending connector-gated actions awaiting a decision — backs the
permission-approval UX (APPROVE / ASK / BLOCK).
| method | what |
|---|---|
list(options?) · sessionsNeedingInput(options?) |
pending approvals · sessions blocked on a decision |
resolve(executionId, decision, scope?) |
approve or deny one — decision: 'approve' | 'deny', scope: 'once' | 'session' | 'session_all' |
p.gateway — LLM observability
Request logs, cost/latency rollups, budgets, and gateway API keys for this project’s model traffic.
| method | what |
|---|---|
logs(opts?) · log(logId) |
request log entries · one log entry |
overview(days?) · series(days?) · breakdown(days?) · sessions(days?) · errors(days?) |
rollups, per-session cost, and errors over a window |
budgets() · setBudget(input) · deleteBudget(budgetId) |
read · create/edit · remove a budget |
keys() · createKey(name) · revokeKey(keyId) |
list · mint · revoke a gateway API key |
playground(prompt, models) |
run one prompt against up to 6 models |
routing.get() · routing.set(policy) · routing.reset() |
read · replace · inherit the routing policy |
routing.preview(input) |
resolve a route without invoking a model |
A routing policy holds a default model, a vision model, and an ordered
fallback chain, each model attempted at most once; fallbackOn is
transient or any-error.
The gateway moves to the next fallback model only when the current model failed before it returned any output:
transient: a network error, a timeout, any5xx, or402,403,429.any-error: every failure, including a400.
A failure after output started stays with that model. A request on your own provider key (BYOK) never falls back to a Beeuni-billed model. Each fallback model gets its own generation defaults.
p.channels — Slack / email / voice
Connector surfaces that let an agent act as a Slack app, an email address, or join a realtime voice call.
| method | what |
|---|---|
slack.installation() · slack.mode() · slack.manifest() |
current install · mode · app manifest |
slack.connect(input) · slack.disconnect() |
connect · disconnect |
slack.getFile(url) · slack.uploadFile(input) |
download · upload a file via the server proxy |
email.installation(connectorSlug?) · email.mode() |
current install · mode |
email.connect(input) · email.disconnect(...) · email.updatePolicy(input) |
connect · disconnect · update the send/reply policy |
voice.setBotName(name) |
rename the bot in a live call |
p.modelDefaults — default model preferences
Account, agent, and project-scoped model defaults, resolved by the gateway.
| method | what |
|---|---|
get() · set(input) · clear(params) |
read · set a default · clear an override |
p.setDefaultAgent — project default agent
p.setDefaultAgent(agentName) checks that the agent is declared and enabled,
then sets it as default_agent in the project’s beeuni.yaml. New sessions
prefer this agent unless a user picks another one.
p.updateFeatureFlag — feature flags
p.updateFeatureFlag(feature, enabled) turns one feature flag on or off for the
project. Pass enabled: null to clear the override and fall back to the
platform default. It calls the canonical PATCH /v1/projects/:id/features.
feature is one of FEATURE_FLAG_KEYS (exported from @beeuni/sdk, typed as
FeatureFlagKey). The caller needs the project’s project.settings.write
permission; the route answers 403 otherwise.
Every flag-gated route rejects the same way while the flag is off: HTTP 403
with { error, code: "feature_disabled", feature }. Use isFeatureDisabledError(error)
to branch on it and featureDisabledKey(error) to read the flag key — never
match on the message text.
p.updateExperimentalFeature(feature, enabled) is the deprecated alias. It
keeps calling the deprecated route alias PATCH /v1/projects/:id/experimental
so consumers pinned to an older deployed API keep working. Use
p.updateFeatureFlag in new code.
p.sandbox — templates and snapshot builds
Sandbox build config beyond sandboxHealth/sandboxTemplates on the project
handle: Dockerfile/image/warm-pool templates and their snapshot builds.
| method | what |
|---|---|
list() · snapshots() |
sandboxes for this project · built snapshots |
rebuildSnapshot(slug?) · fixWithAgent() |
rebuild a snapshot · ask an agent to fix a broken build |
createTemplate(input) · updateTemplate(...) · removeTemplate(...) · buildTemplate(...) |
add · edit · delete · build a template |
setProvider(provider) |
request a provider switch. null (or the platform default / the already-active provider) applies immediately; switching to a different enabled provider starts a durable prepare→verify→activate transition — the current provider keeps serving while the target warm image is built and verified, then activated. The return is a tagged union: kind:'project' (immediate) or kind:'preparation' (poll getProjectSandboxProviderTransition() until activated/failed) |
Platform admin hooks (@beeuni/sdk/react)
These hooks serve the admin console. The API answers them only for a platform
admin. Each mutation invalidates the ['admin', 'accounts'] queries.
| Hook | What it does |
|---|---|
useAdminRole(userId, options?) |
read the caller’s role from GET /user-roles → { isAdmin, role }. userId keys the cache. A missing userId disables the query. Refetches on mount and focus, so a revoked role stops gating within one navigation. |
useAdminGrantCredits() |
mutation { accountId, amount, description, isExpiring }: add wallet credit. POST /admin/api/accounts/:id/credits. Moves money: confirm in the UI first. |
useAdminDebitCredits() |
mutation { accountId, amount, description }: remove wallet credit. POST /admin/api/accounts/:id/credits/debit. Moves money: confirm in the UI first. |
useAdminSetTier() |
mutation { accountId, tier }: set the plan tier. |
useAdminGrantTrial() |
mutation { accountId, tierKey, seats, durationDays, creditGrant?, note? }: grant or replace a trial. The account behaves as tierKey until the window ends. creditGrant also adds wallet credit. |
useAdminRevokeTrial() |
mutation { accountId }: end an active trial now. Answers 400 when none is active. |
Escape hatch
beeuni.runtime() returns the typed runtime REST client for the active
sandbox.
On a client created by createScopedBeeuni (@beeuni/sdk/server), it throws
— the process-global “active” runtime is another request’s sandbox in a
multi-tenant server, a cross-tenant leak. Use the session-scoped
beeuni.session(pid, sid).runtime (call ensureReady() first) instead, which
resolves that session’s own sandbox.
Modules
The framework-free modules behind the client facade and the React hooks. Reach for them when you need one operation without the facade, a pure helper, or a Node-only isolation layer. Each module carries a stability tier so you know what to build on.
| Tier | Meaning |
|---|---|
| Canonical | Import from the root @beeuni/sdk. Use this for all new code. |
| Supported | A dedicated subpath (@beeuni/sdk/react, @beeuni/sdk/server). First-class, not deprecated. |
| Deprecated alias | An old subpath that still works. It re-exports code the root already exports. Import from root instead. |
| Internal | Outside semver. Do not import this in host code. |
The root entry is canonical. Every framework-free name below is importable
straight from @beeuni/sdk:
import { files, getSessionHealth, getClient, authenticatedFetch, backendApi } from '@beeuni/sdk';
Canonical modules
| Module | What it does |
|---|---|
| Files | Workspace file operations: list, read, search, write |
| Session runtime | Health probe and preview/proxy URL builders |
| Runtime client | The typed runtime REST client and its full type surface |
| Auth | authenticatedFetch and token accessors |
| Projects REST | The raw REST functions the facade wraps |
| API client | backendApi, the low-level typed HTTP client |
| Turns | Message-to-turn grouping, cost, and status math — see Turns |
| Transcripts | formatTranscript, a client-side Markdown export |
| Composer lists | The session composer’s agent and model lists: which to offer, which runs |
Files
import { files } from '@beeuni/sdk';
const tree = await files.list('/workspace/src');
const { content } = await files.read('/workspace/README.md');
const hits = await files.findText('TODO');
await files.upload(file, '/workspace/uploads', file.name, {
onProgress: ({ loadedBytes, totalBytes }) => {
console.log(`${loadedBytes}/${totalBytes}`);
},
});
Uploads larger than 64 KiB cross the sandbox edge in verified chunks. The progress callback runs after each chunk reaches the session filesystem.
uploadNativeFile({ uri, name, type? }, targetPath, baseUrl) uploads a device
file from React Native in one request. The runtime streams the bytes from
uri, so they never enter the JS heap. It uses the same auth, deadline and
401 replay as files.upload. It does not chunk.
fetchProjectArchive, fetchCostExportCsv and the sandbox version calls send
through the same transport. A failure throws an ApiError with status.
files.write(path, blob) replaces the file at path and fails if path is a
folder. files.create(path) creates an empty file and never overwrites: when a
file or folder already exists at path, it throws FileExistsError
(status 409, code 'FILE_EXISTS').
import { FileExistsError, files } from '@beeuni/sdk';
try {
await files.create('/workspace/notes.md');
} catch (error) {
if (error instanceof FileExistsError) console.log(`${error.path} already exists`);
else throw error;
}
files targets the globally active sandbox. If your host runs more than one
session at a time, call s.files on the session handle instead. It always
targets that session’s own sandbox. See Sessions.
Session runtime helpers
import { getSessionHealth, isRuntimeReady } from '@beeuni/sdk';
const result = await getSessionHealth();
if (result.ok && isRuntimeReady(result.health)) {
// the sandbox daemon is ready
}
getSessionHealth never throws on a non-2xx status. It returns
{ status, ok, health, body } and lets you decide what a status means. The
same module exports the URL helpers that rewrite an agent’s localhost output
into a reachable proxy URL: detectLocalhostUrls, rewriteLocalhostUrl,
proxyLocalhostUrl, parseLocalhostUrl, and buildWebProxyUrl.
Runtime client
import { getClient } from '@beeuni/sdk';
const client = getClient();
const { data } = await client.session.list({ limit: 100 });
getClient() returns the typed runtime compatibility client for the active sandbox,
with auth already injected. Prefer beeuni.session(pid, sid).runtime, the
same client scoped to one session, over the global getClient() when your
host runs more than one session.
Auth helpers
import { authenticatedFetch, getAuthToken } from '@beeuni/sdk';
const res = await authenticatedFetch(`${runtimeUrl}/beeuni/health`);
const token = await getAuthToken();
The token comes from the getToken function you passed to createBeeuni.
Most app code does not need this module — the file, session, and facade
layers already authenticate for you.
API client
import { backendApi } from '@beeuni/sdk';
const data = await backendApi.get('/some/endpoint');
await backendApi.post('/some/endpoint', { name: 'x' });
backendApi is the typed HTTP client every REST function builds on. Use it
only for an endpoint that has no typed wrapper yet.
Composer agent and model lists
Build a session composer’s agent picker and model picker with the same rules the Beeuni web app uses. Every function is pure: fetch the inputs with the client, then pass them in.
import {
composerSelectableAgents,
createModelVisibility,
flattenModels,
modelInDefaultView,
pickerProviderList,
projectConfigAgentsToRuntimeAgents,
resolveComposerAgent,
resolveComposerModel,
resolveModelDefault,
} from '@beeuni/sdk';
// Agents: the roster, the picker list, and the agent the send carries.
const roster = projectConfigAgentsToRuntimeAgents(detail.config);
const pickerAgents = composerSelectableAgents(roster, { enableProjects: false });
const agent = resolveComposerAgent({
agents: roster,
boundAgent: session?.agent,
defaultAgent: detail.config.default_agent,
selectedAgent: pick,
});
if (agent.disabled) {
// no accessible agent: refuse the send
}
// Models: gateway projects read /model-picker, native projects merge the
// catalog synthesis with the running sandbox's provider list.
const gatewayEnabled = detail.project.experimental?.llm_gateway === true;
const providers = pickerProviderList({
gatewayEnabled,
modelPicker,
runtimeProviders,
llmCatalogProviders,
secretNames,
});
const models = flattenModels(providers, { providerMode: gatewayEnabled ? 'gateway' : 'native' });
const isVisible = createModelVisibility({ catalogModels: models });
const defaultView = models.filter(
(m) => m.enabled !== false && modelInDefaultView(m, { search: '', isStoreVisible: isVisible, selected }),
);
const { model, explicit } = resolveComposerModel({
models,
picks: [sessionPick],
serverDefault: resolveModelDefault(modelDefaults, agent.selected ?? undefined),
providers,
});
resolveComposerModel returns explicit: undefined when the composer is on
its default. model is the key to display and send. Each flattened model’s
id is the ref a pick stores; modelRefToKey(ref, gatewayEnabled) reads a
stored ref (a session pin, a channel binding, a trigger’s model) back into a
key.
Supported subpaths
| Subpath | What it does |
|---|---|
@beeuni/sdk/react |
React hooks — see React hooks |
@beeuni/sdk/server |
Request-scoped config for multi-tenant backends |
@beeuni/sdk/wire-message-id |
The runtime wire message-id clock alone (one module, no imports). The root exports the same names |
Wire message ids
A prompt’s messageId is its position in the transcript. The runtime orders
messages by msg_ + a 48-bit clock (Date.now() * 0x1000, low 48 bits) +
14 base62 characters, and the clock wraps every 2^36 ms (about 2.2 years).
Never encode it by hand.
import { mintWireMessageId } from '@beeuni/sdk';
// `after`: the ids already in this session's transcript.
const messageId = mintWireMessageId({ after: transcriptIds });
The mint is dated two minutes back, then lifted one tick above the newest id
in after that is at most one hour ahead. A caller that holds the newest
position as a clock instead of ids uses mintWireMessageIdAbove({ nowMs, newestKnownTime }), which returns { id, time }. newestWireIdClock(ids)
finds that clock. Every comparison is on the ring, so ids on both sides of a
wrap keep their true order.
The 48-bit clock wraps every ~2.2 years, so never compare two clocks with >
or <=: an id minted just after a wrap is numerically smaller than one minted
just before it. Use wireIdClockDelta(clock, reference), the signed distance
on the ring (positive when clock is ahead), and maxWireIdClock(clocks) for
the newest of several:
import { maxWireIdClock, wireIdClockDelta } from '@beeuni/sdk/wire-message-id';
const floor = maxWireIdClock([transcriptNewest, deliveredNewest]);
const stale = floor !== null && wireIdClockDelta(clientClock, floor) <= 0n;
Server-side isolation
createBeeuni stores its config, including the token function, in one
process-wide variable. That is fine for a browser tab, a CLI, or a
single-tenant server. It is unsafe for a Node server that handles concurrent
requests for different users, because the last createBeeuni call wins for
every in-flight request. @beeuni/sdk/server fixes this with per-request
isolation:
import { createScopedBeeuni } from '@beeuni/sdk/server';
export async function handler(req: Request) {
const beeuni = createScopedBeeuni({ backendUrl, getToken: () => tokenFor(req) });
return beeuni.projects.list();
}
createScopedBeeuni and runWithBeeuni isolate config per request with
Node’s AsyncLocalStorage. Never import @beeuni/sdk/server from a browser
bundle — it statically pulls in node:async_hooks.
A scoped client’s top-level runtime() throws (it would resolve another
tenant’s sandbox). Reach a specific session’s runtime via
beeuni.session(pid, sid).runtime after await s.ensureReady().
Deprecated aliases
About twenty old subpaths still work: /files, /turns, /session, /auth,
/projects-client, /api-client, /config, /event-stream,
/opencode-client, /platform-client, and more. Each one re-exports code the
root @beeuni/sdk entry already exports. They stay working so no existing
import breaks, but new code should import from the root.
Internal modules
@beeuni/sdk/internal/* holds the zustand stores apps/web’s own runtime uses
internally — session sync state, active-runtime tracking, and reconnect
bookkeeping. This subpath is explicitly outside semver. Do not import it in
host code; the React hooks already expose the state you
need.
Turns
Plain functions that group session messages into turns and classify each
message part. Use them to build a custom chat renderer instead of the
reference one in apps/web. No React, no DOM — every export is a plain
function or type, safe to call from any host.
import { classifyPart, classifyTurn, toolInfo, toolViewModel } from '@beeuni/sdk';
Import every function from the root @beeuni/sdk entry. The
@beeuni/sdk/turns subpath still works, but it is a deprecated alias. New
code must use the root entry — see Distribution.
Classify a part
classifyPart(part) normalizes one of the 12 part types of the Beeuni
transcript format (beeuni.transcript.v1, BEEUNI_TRANSCRIPT_SCHEMA) into a
ClassifiedPart — a union keyed by kind. Each variant already resolves the
fields a renderer needs: tool status, parsed JSON output, image detection.
import { classifyPart, type ClassifiedPart } from '@beeuni/sdk';
for (const part of message.parts) {
const classified: ClassifiedPart = classifyPart(part);
switch (classified.kind) {
case 'text':
render(classified.text);
break;
case 'tool':
render(classified.tool.title, classified.tool.status);
break;
}
}
kind |
shape |
|---|---|
text |
{ id, text, synthetic } — skip synthetic parts; they mark shell mode’s synthetic prompt |
reasoning |
{ id, text } |
tool |
{ id, tool: ToolView } |
file |
{ id, filename?, mime, url, isImage, isPdf } |
subtask |
{ id, description, agent, prompt, model? } |
patch |
{ id, hash, files, fileCount } |
snapshot |
{ id, snapshot } |
agent |
{ id, name } |
retry |
{ id, attempt, message, createdAt } |
compaction |
{ id, auto, overflow, tailStartId? } |
step |
{ id, phase: 'start' | 'finish', snapshot?, reason?, cost?, tokens? } |
unknown |
{ raw } — a part type this SDK version does not know |
An unrecognized wire part degrades to unknown at runtime instead of
throwing. This lets an older client talk to a newer server.
A tool part classifies into ToolView:
interface ToolView {
name: string;
title: string;
status: 'pending' | 'running' | 'done' | 'error';
input?: Record<string, unknown>;
output?: string;
error?: string;
outputParsed?: unknown; // JSON.parse(output) when it parses, capped at 256KB
outputText?: string; // the raw output text, always present
kind?: ToolKind; // toolKind(name): the action, whatever the harness calls the tool
files?: ToolFile[]; // the files a file tool read, wrote or changed: path, patch, additions/deletions
diff?: string; // a unified diff of the change
diagnostics?: Record<string, unknown[]>; // language-server diagnostics, keyed by file path
answers?: string[][]; // the answers to a `question`, one list per question
childSessionId?: string; // the child session a delegating call (`task`) runs
}
toToolView(part) builds one straight from a tool part. Read these fields
instead of a harness’s own state.metadata: the same card then renders a
runtime edit (filePath, filediff) and a pi edit (path, edits,
a unified patch).
Some tools (web_search, image_search, connector calls) report
state.status: 'completed' even when their JSON body carries
success: false. classifyPart detects this and sets ToolView.status to
'error' in that case too.
classifyTurn(message) classifies every part of one assistant message and
returns a ClassifiedTurn with three fields:
parts— each part, classifiederror— frommessage.info.error, if anyisEmpty— true when the turn has no error and no part with visible content
Tool metadata
toolKind(name) is the one tool taxonomy: it maps any harness’s tool name
(pi’s write, runtime’s oc-apply_patch, a plugin’s pty_spawn) to a
ToolKind — read, edit, write, bash, web_search, task,
question, … or other. Narration families, view models, toolInfo
categories and the context-tool group all derive from it. inputPath(input)
reads a file tool’s path from filePath, file_path or path.
toolKind('oc-apply_patch'); // 'apply_patch'
toolKind('pty_spawn'); // 'pty'
inputPath({ path: 'app.py' }); // 'app.py'
Two more lookups both describe a tool. Do not confuse them.
toolInfo(name)— icon-free, returns{ label, category }.classifyPartuses this internally.ToolCategoryis'shell' | 'files' | 'search' | 'edit' | 'web' | 'task' | 'other'.getToolInfo(name, input)— icon-aware, returns{ icon, title, subtitle }for the reference tool-card UI. The subtitle comes from the tool’s input, for example a file path or a search query.
toolInfo('bash'); // { label: 'Shell', category: 'shell' }
getToolInfo('write', { filePath: '/workspace/main.go' });
// { icon: 'file-pen', title: 'Write', subtitle: 'main.go /workspace' }
Both functions match tool-name prefixes, so Beeuni’s plugin tool families
(agent_*, session_*, task_*, trigger_*, project_*, pty_*) resolve
without a registry update. An unknown tool name never throws — it falls back
to humanizeToolName(name) with category 'other'.
Tool view models
toolViewModel(classifiedTool) maps a classified tool part to a shape built
for one tool family. A UI can then render it specially instead of as a
generic JSON blob.
const vm = toolViewModel(classifiedTool);
if (vm.kind === 'shell') {
render(vm.command, vm.stdout, vm.exitCode);
}
kind |
shape | tools |
|---|---|---|
web-search |
{ query, results?, answer?, error? } |
web_search, image_search |
shell |
{ command, stdout?, exitCode? } |
bash |
file-read |
{ path, preview? } |
read |
file-write |
{ path, preview? } |
write |
file-edit |
{ path, diff?: DiffLine[] } |
edit, morph_edit |
search |
{ pattern, matches?: SearchMatch[] } |
grep, glob |
task |
{ description, agent? } |
task |
todo |
{ items: TodoItem[] } |
todowrite |
question |
{ questions: QuestionItem[], answers? } |
question, ask |
generic |
{ label, inputPretty?, outputPretty? } |
everything else, always safe to render |
DiffLine is { type: 'added' | 'removed' | 'unchanged', text }. String
fields are capped so one large tool output never breaks a render: 4000
characters for pretty-printed JSON, 256KB before a diff runs.
Group messages into turns
A turn pairs one user message with the assistant messages that answered it. It is the unit a chat UI renders as one exchange.
import { groupMessagesIntoTurns, collectTurnParts, type TurnLike } from '@beeuni/sdk';
const turns: TurnLike[] = groupMessagesIntoTurns(messages);
for (const turn of turns) {
const parts = collectTurnParts(turn);
}
groupMessagesIntoTurns links each assistant message to its parent user
message. It falls back to message order when a parent link is missing. It
also attaches an orphan assistant message — one with no parent that precedes
every user message — to the first turn, not the last.
Related helpers:
findLastTextPart(parts)— the turn’s final response textturnHasSteps(parts)— true if atool,compaction,snapshot, orpatchpart existsisShellMode(turn)/getShellModePart(turn)— a turn that is one synthetic prompt driving onebashcall
Type guards
Narrow a part by type:
import { isTextPart, isToolPart, getPartText } from '@beeuni/sdk';
if (isTextPart(part)) {
// part.type narrowed to 'text'
}
const text = getPartText(part); // works for 'text' and 'reasoning' parts
Also available: isReasoningPart, isFilePart, isAgentPart,
isCompactionPart, isSnapshotPart, isPatchPart, isStepPart
(step-start and step-finish).
Status and errors
import { getWorkingState, getTurnStatus, formatDuration } from '@beeuni/sdk';
const status = getTurnStatus(parts, childMessages); // "Running commands..."
formatDuration(4300); // "4s" — durations under 1s return ''
getTurnStatus scans a turn’s parts for the last status line. When the last
part is a running task delegation, pass childMessages so the status shows
the sub-agent’s real activity instead of a generic “Delegating…” line.
import { getTurnError, getChildSessionError, unwrapError } from '@beeuni/sdk';
getTurnError(turn); // the first assistant error, unwrapped
getChildSessionError(childMessages); // newest error in a sub-agent's messages
unwrapError(rawError); // normalizes double-JSON and mixed error shapes
Cost and token totals
import { getTurnCost, getSessionCost, formatCost, formatTokens } from '@beeuni/sdk';
const info = getTurnCost(partsWithMessage, modelPricingLookup);
const sessionCost = getSessionCost(messages, modelPricingLookup);
formatCost(0.0032); // "$0.003"
formatTokens(12345); // "12k"
Both functions read cost and token totals from step-finish parts. When a
part reports zero, they estimate cost from token counts using a
ModelPricingLookup.
ChatGPT/Codex subscription messages contribute $0.00, including historical
messages with nonzero runtime costs. Token and context usage remain visible.
Mixed sessions retain charges for paid API messages. OpenAI API models remain
billable, including API models whose names contain codex.
Child sessions and pending requests
A project session’s conversation tree (the root and the subagent
conversations it spawned) comes from its runtime_sessions list. These
helpers read it, preferring the current names over the pre-W4 ones:
import { directSubsessions, projectSessionForRuntimeId, rootRuntimeSession, runtimeSessionsOf } from '@beeuni/sdk';
const root = rootRuntimeSession(projectSession); // the root conversation id
const children = directSubsessions(projectSession); // its subagent conversations
const owner = projectSessionForRuntimeId(projectSessions, childId); // the session a conversation belongs to
A task tool call delegates to a child session. These helpers connect a
parent turn to that child’s own messages.
import { getChildSessionId, getChildSessionToolParts } from '@beeuni/sdk';
const childId = getChildSessionId(taskToolPart);
const steps = getChildSessionToolParts(childMessages);
Match a pending permission or question request to its tool call, and find which tool parts to hide while one is active:
import { getPermissionForTool, getHiddenToolParts, isToolPartHidden } from '@beeuni/sdk';
const permission = getPermissionForTool(permissions, callID);
const hidden = getHiddenToolParts(activePermission, activeQuestion);
Formatting and lists
import { getFilename, getDirectory, relativizePath, stripAnsi } from '@beeuni/sdk';
getFilename('/workspace/src/main.go'); // "main.go"
getDirectory('/workspace/src/main.go'); // "/workspace/src"
relativizePath('/workspace/src/main.go', '/workspace'); // "src/main.go"
Session-list helpers operate on the same session data:
import { sortSessions, childMapByParent, allDescendantIds } from '@beeuni/sdk';
sessions.sort(sortSessions(Date.now())); // pins sessions updated in the last 60s
const childMap = childMapByParent(sessions);
const descendants = allDescendantIds(childMap, sessionId);
Retry state: getRetryInfo(sessionStatus) returns { attempt, message, next, details? } when status.type === 'retry', with message capped to 60
characters. details preserves a structured LLM-gateway envelope when present:
the final provider, gateway code, request ID, suggestion, upstream status, and
ordered candidate failures. Each candidate failure contains attempt,
provider, routeModel, resolvedModel, stage, optional status, code,
and message. Legacy plain-text retries return details: undefined.
getRetryMessage(sessionStatus) returns the full unwrapped message. When
the runtime retains only that message, the gateway composite still includes the
request ID and each candidate’s provider, resolved model, HTTP status, code,
and bounded message.
Structural types
The grouping and status functions accept minimal structural types —
PartLike, MessageInfoLike, TurnLike, ToolStateLike,
SessionStatusLike — instead of the concrete transcript types.
Your own message and part shapes flow through unchanged as long as they match
the required fields. classifyPart and classifyTurn are the exception.
They type against the Beeuni Part union (beeuni.transcript.v1), so their
exhaustiveness check catches a new part type at build time.
Distribution
npm install @beeuni/sdk
The package ships as compiled ESM with full TypeScript type declarations.
react and @tanstack/react-query are optional peer dependencies. If you
use @beeuni/sdk/react, install both.
import { createBeeuni } from '@beeuni/sdk'; // framework-free core
import { useSession } from '@beeuni/sdk/react'; // optional React layer
import { createScopedBeeuni } from '@beeuni/sdk/server'; // Node and Bun servers
Entry points and stability
The root entry, @beeuni/sdk, is canonical. It exports the full
framework-free surface and runs in browsers, Node 18+, Bun, and edge
runtimes.
| Entry | Tier | Contract |
|---|---|---|
@beeuni/sdk |
Canonical | Framework-free. Never imports React or node:*. |
@beeuni/sdk/react |
Supported | React hooks. The only entry that imports React. |
@beeuni/sdk/server |
Supported | Per-request config isolation for Node and Bun servers, via node:async_hooks. Never bundle it into a browser. |
Legacy subpaths (/projects-client, /turns, /files, and more) |
Deprecated | Still work. Each re-exports from the root. Import from the root instead. |
@beeuni/sdk/internal/* |
Internal | Used by the Beeuni web app only. Not a supported API. |
CDN bundles
The package also ships two browser bundles, built by tsup: an ESM bundle
(dist/beeuni.esm.min.js) and an IIFE global (dist/beeuni.global.js) that
defines window.Beeuni.
<script src="https://unpkg.com/@beeuni/sdk/dist/beeuni.global.js"></script>
<script>
const beeuni = Beeuni.createBeeuni({
backendUrl: 'https://api.beeuni.ai/v1',
getToken: async () => KEY,
});
</script>
window.Beeuni exposes the same root entry as the npm import:
Beeuni.createBeeuni, Beeuni.classifyTurn, Beeuni.ApiError.
See also
- SDK — install and your first session.
- Sessions — lifecycle, streaming, and error handling.
- React hooks — the reactive layer built on these modules.
Agent repository access
Agent configuration accepts repository_access?: boolean (default true).
Set false to run new sessions without the project repository or repository API access.
Git, secret, connector, and tool permissions remain separate. Existing sessions retain their saved policy.
AgentConfigBlock.workspace is deprecated. The SDK maps legacy branch/runtime to the boolean field.
A legacy read write requires an explicit repository_access choice; it does not enable read-only repository access.
Provider and model access
Read beeuni.projects.modelAccess(projectId). Change one target with beeuni.projects.setModelAccess(projectId, { target: 'provider' | 'model', id, enabled }).
Use provider ID beeuni for Beeuni Managed Models, or a catalog provider ID for BYOK, Codex, and custom providers. Use the full gateway wire ID for a model. Provider disable covers future models and preserves individual model preferences and saved credentials.
The current default model and its provider require a replacement before disabling (409 cannot_disable_default). Direct gateway requests to disabled targets fail with model_disabled or provider_disabled. Existing in-flight upstream requests finish normally. The response’s enforced flag is false when the project uses a native runtime model connection instead of the gateway.
React consumers use useModelAccess(projectId) from @beeuni/sdk/react. Its setEnabled(change) mutation refreshes the management catalog and session picker.