@byokit/realtime

中文文档
0.3.3发布于 2026/10/25Apache-2.0TypeScriptES Module构建来源已验证近 30 天下载 2734

TypeScript kits that let people sign in to your app with the AI plans they already pay for, on web, phone and desktop.

原始使用文档
文档目录5 节
  1. Structured delegation and named targets
  2. Advisory auth check
  3. Lifecycle announcements and semantic app tools
  4. Preconnect without microphone capture
  5. Reconnect and host policy

@byokit/realtime

Realtime voice sessions with injected audio and provider adapters in a child process. First consumer: muxr.

import { realtimeClient, type AudioPorts, type RealtimeHostFrame, type RealtimeStream } from '@byokit/realtime';
import { realtimeEngine, toolBridge } from '@byokit/realtime/node';

// The app supplies its sign-in, carrier, audio implementation and authorized planner.
declare const accounts: { access(member: number, signal?: AbortSignal): Promise<{ access: string; accountId: string }> };
declare const emit: (frame: RealtimeHostFrame) => void;
declare const runPlanner: (request: string, signal: AbortSignal) => Promise<string>;
declare const openVoiceStream: () => Promise<RealtimeStream>;
declare const audio: AudioPorts;
const member = 1;
const tools = [{ name: 'delegate', description: 'Handle a spoken request', parameters: { type: 'object', properties: { request: { type: 'string' } } } }];
const bridge = toolBridge({
  tools, handlers: { delegate: async (args, { signal }) => runPlanner(String(args.request), signal) },
  emit, failure: () => 'That request could not be completed.',
});
const engine = realtimeEngine({
  engine: 'chatgpt', auth: { kind: 'plan', access: signal => accounts.access(member, signal) },
  tools, bridge, instructions: 'Answer briefly.', emit,
});
// Forward device frames to engine.receive(frame); engine.close() ends the child.
const client = realtimeClient({
  open: openVoiceStream, audio,
  onStatus: status => console.log(status),
  onTurn: (role, text) => console.log(role, text),
});

For ChatGPT voice register a delegate tool handler receiving { request } and supply the app's planner. API routes use auth: { kind: 'key', key }, with billing per use. The subscription route uses the person's ChatGPT sign-in from @byokit/accounts; it never reads Codex credentials or starts the Codex CLI. Backend refusal is reported as not-included.

Structured delegation and named targets

delegationHandler dispatches a JSON request of the form { name, arguments } directly through an app-supplied toolBridge. For ordinary spoken requests it uses the optional app planner. Invalid structured requests never enter the planner. Keep the action bridge separate from the outer delegate bridge:

import { delegationHandler, toolBridge } from '@byokit/realtime/node';
import type { ToolHandler } from '@byokit/realtime/node';
import type { RealtimeHostFrame } from '@byokit/realtime';

declare const emit: (frame: RealtimeHostFrame) => void;
declare const sendAuthorizedMessage: ToolHandler;
declare const plan: (request: string, context: Parameters<ToolHandler>[1]) => Promise<string>;
const actionTools = [{ name: 'message', description: 'Message a named agent', parameters: {
  type: 'object', properties: { agent: { type: 'string' }, text: { type: 'string' } },
  required: ['agent', 'text'], additionalProperties: false,
} }];
const actions = toolBridge({ tools: actionTools, handlers: { message: sendAuthorizedMessage },
  emit, failure: () => 'The message could not be confirmed.', timeoutFor: () => 75000 });
const delegate = delegationHandler({ bridge: actions, plan });
const tools = [{ name: 'delegate', description: 'Delegate a spoken request', parameters: { type: 'object' } }];
const bridge = toolBridge({ tools, handlers: { delegate }, emit,
  timeoutFor: () => 340000, failure: () => 'The request could not be confirmed.' });
// Pass the outer bridge and its delegate tool to realtimeEngine.
// A structured request can be:
// {"name":"message","arguments":{"agent":"Avery","text":"Please report progress"}}
// Close actions as well as the outer bridge when the session ends.

The kit preserves arguments and operation ids; retries with the same call id share a result. Named targets, target lookup, ambiguity checks, authorization, argument validation and mutation receipts belong to the app's handlers. Tool JSON schemas describe the provider catalog; they are not runtime authorization. The app planner owns its model, conversation memory, tool budget and deadline; the kit does not add a planner model or any agent runtime dependency. Aborted calls must stop in the app too; completed side effects are never rolled back. The host bridge owns every tool watchdog and failure message; the child has no second deadline. A 340-second planner or a 290-second watch can therefore use the app's declared budget. An ordinary ChatGPT user turn preserves unfinished delegations and their retransmission cache. Explicit interrupt or close aborts them; a new turn id still identifies a new request.

Advisory auth check

Pass a read-only saved-sign-in lookup as authCheck on the engine. It starts independently, is never awaited by startup, and cannot close the session. It does not call auth.access, refresh a token, run a CLI or initiate login. The app's peek callback must also never refresh, prompt or start login.

import { realtimeEngine, toolBridge } from '@byokit/realtime/node';
import type { RealtimeAuth, RealtimeHostFrame, RealtimeTool, RealtimeAuthPeekResult, RealtimeAuthResult } from '@byokit/realtime';

declare const auth: Extract<RealtimeAuth, { kind: 'plan' }>;
declare const tools: RealtimeTool[];
declare const bridge: ReturnType<typeof toolBridge>;
declare const emit: (frame: RealtimeHostFrame) => void;
declare const peekSavedSignIn: (signal: AbortSignal) => Promise<RealtimeAuthPeekResult>;
declare const updateSettingsBadge: (status: 'ready' | 'signed-out' | 'unknown') => void;
declare const showSignInMessage: (result: RealtimeAuthResult) => void;
const engine = realtimeEngine({
  engine: 'chatgpt', auth,
  tools, bridge, emit,
  authCheck: { peek: peekSavedSignIn, onStatus: status => updateSettingsBadge(status), onResult: showSignInMessage },
});

ready means the app reports a saved sign-in, not provider entitlement. signed-out means the lookup returned false or found an unusable saved sign-in. Unrecognized errors or a timeout produce unknown; the default timeout is one second, capped at ten seconds. Close cancels pending checks and suppresses late notifications. Normal session authentication still needs the credential from auth.access. For a standalone settings lookup, realtimeAuthCheck({ peek }) is also exported from the portable entry and returns { result, details, close }. result still resolves to the status string; details resolves to { state, reason?, message? }, and engine onResult receives the same diagnostic object. A boolean or { state } peek keeps the previous behavior without adding a reason.

To report an expired saved sign-in, the read-only peek can return { state: 'signed-out', reason: 'login-expired' }. Built-in reasons are credential-permissions, login-expired, missing and unknown; apps can supply additional reason strings. Messages use fixed plain wording and never copy exception text or credential paths. Missing-file errors map to missing, permission errors to credential-permissions, and expired-login errors to login-expired; unrecognized errors and timeouts report unknown. Do not await this check before opening a voice session or show a login prompt from its result.

Lifecycle announcements and semantic app tools

Feed app-confirmed lifecycle reports into the existing speech path. The kit does not watch agents or infer their outcome:

import type { realtimeClient } from '@byokit/realtime';
import type { realtimeEngine } from '@byokit/realtime/node';
declare const client: ReturnType<typeof realtimeClient>;
declare const engine: ReturnType<typeof realtimeEngine>;

// Called by the app's lifecycle subscription; use bounded, sanitized app wording.
client.speak('Avery has finished the task.');
// A host can inject the same report directly:
engine.receive({ type: 'realtime.say', text: 'Avery has finished the task.' });

speak queues at most sixteen reports until the call is ready, waits for local playback drain and does not replay sent speech on reconnect. onTurn observes transcripts and onStatus observes playback states; a transcript alone does not prove the report was audible. The app owns push delivery, deduplication, waking a sleeping call and stopping it after reports. When player.finish(callback) returns false, the client reports connected immediately. True means the player will call back after draining. Clearing audio also rechecks queued speech, including while the microphone is muted.

Compose appBridge with toolBridge to round-trip semantic phone requests:

import { appBridge, toolBridge } from '@byokit/realtime/node';
import type { RealtimeHostFrame } from '@byokit/realtime';
declare const emit: (frame: RealtimeHostFrame) => void;
const app = appBridge(emit);
const navigation = [{ name: 'navigate', description: 'Open an app destination',
  parameters: { type: 'object', properties: { target: { type: 'string' } }, required: ['target'] } }];
const bridge = toolBridge({ tools: navigation, app, emit,
  handlers: { navigate: (args, { signal }) => app.run('navigate', String(args.target), signal) },
  failure: () => 'The app could not complete that request.',
});
// client.onAppRequest handles view/navigate/activate; forward all client frames
// to engine.receive, which routes app results back to this app bridge.
// Closing the engine also cancels pending app requests.

The main entry is portable for web and React Native and loads no native code. ./webrtc supplies webRtcPeer for browsers and selects its native implementation under the react-native condition; install the optional react-native-webrtc 124.0.8 peer in the RN app. All media APIs can be injected for tests. ./node requires Node 22.18+ and is for the credential host. It starts its own adapter child with an empty environment; credentials travel over private stdin. No provider key or plan token is sent to the device.

AudioPorts requires microphone acquisition/release, PCM capture, playback admission/drain and audio routing. The app owns permissions and its microphone service. Acquire must resolve only after that service is ready. The client awaits it before capture. Each client owns its ports; the app must arbitrate shared microphones.

Preconnect without microphone capture

Set capture: 'lazy' for a browser or React Native WebRTC session. Startup negotiates one sendrecv audio transceiver with no sender track: it calls neither audio.microphone.acquire() nor getUserMedia, so the app's microphone foreground service stays stopped. The playback route remains available.

import { realtimeClient, type AudioPorts, type RealtimeStream } from '@byokit/realtime';
import { webRtcPeer } from '@byokit/realtime/webrtc';
declare const audio: AudioPorts;
declare const openVoiceStream: () => Promise<RealtimeStream>;
const client = realtimeClient({
  open: openVoiceStream, audio, webrtc: webRtcPeer, capture: 'lazy',
  onStatus() {}, onTurn() {},
});
// Once WebRTC setup has started, await this before accepting speech:
async function beginTalking() { await client.attachMic(); }
// When idle, release hardware capture while keeping the call connected:
async function becomeIdle() { await client.releaseMic(); }
// End the call and release all media:
function endCall() { client.stop(); }

attachMic() awaits the app's microphone permission/service lease, captures an audio track and uses sender.replaceTrack(track) before enabling the sender's encoding. Lazy peers negotiate sendrecv with an inactive encoding: Android's audio device cannot start recording just because an answer is applied. releaseMic() disables that encoding before replaceTrack(null), stops capture tracks and releases the lease so the app can stop its microphone service and clear the OS indicator. Neither operation renegotiates. Repeated calls are safe; release or stop during pending capture cleans up when that capture resolves. Reconnecting starts another lazy peer; the app explicitly attaches again when it needs input.

The built-in webRtcPeer handle also exposes both methods for direct use. Custom peer factories may implement these optional methods; client attach rejects if the current peer does not support them or setup has not started. The default is eager capture, and PCM capture is unchanged. setMuted() still only toggles track delivery; use releaseMic() to relinquish capture. Mute state persists when a new track is attached. Each client owns its AudioPorts; microphone.release() must stop the app-owned foreground service.

Close the client and engine when the call ends. client.interrupt() clears playback and aborts unfinished tools; old response output is fenced. Transient provider and transport failures retry twice, after 500 and 1,000 ms, with a fresh budget after 30 healthy seconds. Sent audio, speech and tool calls are never replayed. The app can mark transport closure terminal with retryableClose. Explicit ChatGPT interruption opens a fresh call; explicit Gemini interruption rotates its provider session. Native speech interruption remains provider-driven. No STT/LLM/TTS chain is exported. See the lift contract for bounds, events and usage.

Reconnect and host policy

Set preserveMediaOnReconnect: true on the client to retain a ready PCM recorder, microphone lease, audio route and playback tail across a carrier reconnect. Audio captured while the carrier is down is discarded. Pending microphone acquisition still releases before retry; WebRTC always creates a fresh peer for a fresh call. A changed PCM format releases the old recorder before acquiring another. Stop releases retained media even during backoff. The default remains release/clear/reacquire.

The engine accepts app phrase policy and credential-error wording:

import { realtimeEngine, toolBridge } from '@byokit/realtime/node';
import type { RealtimeAuth, RealtimeHostFrame, RealtimeTool } from '@byokit/realtime';
declare const auth: Extract<RealtimeAuth, { kind: 'plan' }>;
declare const tools: RealtimeTool[];
declare const bridge: ReturnType<typeof toolBridge>;
declare const emit: (frame: RealtimeHostFrame) => void;

const engine = realtimeEngine({ engine: 'chatgpt',
  auth, tools, bridge, emit,
  hangup: text => /^goodbye[.!]?$/i.test(text.trim()),
  authFailure: () => 'Sign in again from Settings.',
  signalingIdentity: { originator: 'voice-app', userAgent: 'voice-app/1' },
});

Hangup ends after the next completed agent transcript so the provider can give a farewell. It is a transcript-level policy, not proof that playback drained. Credential errors are sanitized, bounded and emitted as the close reason; the kit initiates no login or prompt. Transcripts preserve up to 4,000 UTF-8 bytes, state details up to 500 bytes, and close reasons up to 2,048 bytes. Empty ChatGPT transcripts are ignored.

ChatGPT emits realtime.webrtc.start while the isolated child starts and access is being resolved. Device frames remain bounded and queued until the credential/config line is sent. This overlaps local setup without sending a signaling request before credentials are available. signalingIdentity lets the app name its own originator/user-agent; the default for both is byokit, and identity values must be printable ASCII of at most 160 characters. Credentials, endpoint restrictions and redirect rejection are unchanged. The identity option allows a consumer to compare signaling latency; offline tests prove startup ordering, not a reduction in the remote provider's POST time.

For an explicitly live microphone and subscription-provider proof, run the bounded voice demo. It signs in through accounts, keeping the sign-in at this computer's machine store, and records connection facts and turn counts. Offline tests do not establish a live pass.

版本与发布记录

@byokit/realtime 共发布 7 个版本,首次发布于 2026/09/30;过去 12 个月发布了 7 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.3.3latest2026/10/2514 天前
0.3.22026/10/1722 天前
0.3.12026/10/011 个月前
0.3.02026/10/011 个月前
0.2.12026/10/011 个月前
0.2.02026/10/011 个月前

关于 @byokit/realtime 的常见问题

@byokit/realtime 是什么?

TypeScript kits that let people sign in to your app with the AI plans they already pay for, on web, phone and desktop.

如何安装 @byokit/realtime?

在项目目录运行 npm install @byokit/realtime,或使用 pnpm add / yarn add / bun add。

@byokit/realtime 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@byokit/realtime 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@byokit/realtime 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.18。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@byokit/realtime 可以商用吗?

@byokit/realtime 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@byokit/realtime 的下载量有多少?

根据 npm 官方下载统计,@byokit/realtime 在 2026/10/09 – 2026/11/07 这 30 天内被下载 2,734 次,日均约 91 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@byokit/realtime 还在维护吗?

最新版本 0.3.3 发布于 2026/10/25(14 天前);至今共发布 7 个版本,其中过去 12 个月发布了 7 个;首次发布于 2026/09/30。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。