@rimelabs/sdk 0.1.0-alpha.2 → 0.1.0-alpha.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/LICENSE-PRISM +201 -0
  3. package/NOTICE-PRISM +9 -0
  4. package/README.md +278 -114
  5. package/dist/auth.d.ts +9 -3
  6. package/dist/auth.js +12 -4
  7. package/dist/cancellation.d.ts +6 -0
  8. package/dist/cancellation.js +54 -0
  9. package/dist/client.d.ts +24 -23
  10. package/dist/client.js +41 -92
  11. package/dist/grpc.d.ts +27 -0
  12. package/dist/grpc.js +175 -0
  13. package/dist/index.d.ts +10 -2
  14. package/dist/index.js +6 -2
  15. package/dist/pcm.d.ts +21 -0
  16. package/dist/pcm.js +52 -0
  17. package/dist/realtime/async.d.ts +23 -0
  18. package/dist/realtime/async.js +87 -0
  19. package/dist/realtime/audio.d.ts +2 -0
  20. package/dist/realtime/audio.js +11 -0
  21. package/dist/realtime/client.d.ts +12 -0
  22. package/dist/realtime/client.js +206 -0
  23. package/dist/realtime/conversation.d.ts +47 -0
  24. package/dist/realtime/conversation.js +309 -0
  25. package/dist/realtime/protocol.d.ts +84 -0
  26. package/dist/realtime/protocol.js +397 -0
  27. package/dist/realtime/session.d.ts +69 -0
  28. package/dist/realtime/session.js +744 -0
  29. package/dist/realtime/transport.d.ts +16 -0
  30. package/dist/realtime/transport.js +83 -0
  31. package/dist/realtime/types.d.ts +240 -0
  32. package/dist/realtime/types.js +10 -0
  33. package/dist/stt/audio.d.ts +15 -0
  34. package/dist/stt/audio.js +46 -0
  35. package/dist/stt/client.d.ts +17 -0
  36. package/dist/stt/client.js +121 -0
  37. package/dist/stt/policy.d.ts +11 -0
  38. package/dist/stt/policy.js +11 -0
  39. package/dist/stt/protocol.d.ts +14 -0
  40. package/dist/stt/protocol.js +76 -0
  41. package/dist/stt/queue.d.ts +16 -0
  42. package/dist/stt/queue.js +45 -0
  43. package/dist/stt/stream.d.ts +48 -0
  44. package/dist/stt/stream.js +224 -0
  45. package/dist/stt/transport.d.ts +15 -0
  46. package/dist/stt/transport.js +94 -0
  47. package/dist/stt/types.d.ts +51 -0
  48. package/dist/stt/types.js +1 -0
  49. package/dist/{audio.d.ts → tts/audio.d.ts} +9 -0
  50. package/dist/{audio.js → tts/audio.js} +44 -4
  51. package/dist/tts/buffer.d.ts +3 -0
  52. package/dist/tts/buffer.js +53 -0
  53. package/dist/tts/client.d.ts +53 -0
  54. package/dist/tts/client.js +151 -0
  55. package/dist/tts/lexicon.d.ts +7 -0
  56. package/dist/tts/lexicon.js +23 -0
  57. package/dist/{policy.d.ts → tts/policy.d.ts} +2 -3
  58. package/dist/tts/policy.js +41 -0
  59. package/dist/{stream.d.ts → tts/stream.d.ts} +15 -2
  60. package/dist/{stream.js → tts/stream.js} +54 -30
  61. package/dist/tts/timestamps.d.ts +24 -0
  62. package/dist/tts/timestamps.js +46 -0
  63. package/dist/{transport.d.ts → tts/transport.d.ts} +10 -13
  64. package/dist/tts/transport.js +133 -0
  65. package/dist/validation.d.ts +5 -0
  66. package/dist/validation.js +28 -0
  67. package/docs/realtime.md +426 -0
  68. package/docs/stt.md +186 -0
  69. package/docs/tts.md +284 -0
  70. package/package.json +14 -12
  71. package/dist/policy.js +0 -82
  72. package/dist/sentences.d.ts +0 -21
  73. package/dist/sentences.js +0 -142
  74. package/dist/transport.js +0 -223
  75. package/vendor/LICENSE.blingfire +0 -21
  76. package/vendor/README.md +0 -11
  77. package/vendor/blingfire.cjs +0 -4
  78. package/vendor/blingfire.upstream.cjs +0 -1
  79. package/vendor/blingfire.wasm +0 -0
  80. /package/dist/{queue.d.ts → tts/queue.d.ts} +0 -0
  81. /package/dist/{queue.js → tts/queue.js} +0 -0
@@ -0,0 +1,133 @@
1
+ import { connect as connectGrpc } from "../grpc.js";
2
+ export { metadata } from "../grpc.js";
3
+ import { BidiCall, serializer, deserializer, requestId, nativeError, } from "../grpc.js";
4
+ export { rpcError } from "../grpc.js";
5
+ import * as grpc from "@grpc/grpc-js";
6
+ import { create, } from "@bufbuild/protobuf";
7
+ import * as schema from "@rimelabs/api";
8
+ import { policy } from "./policy.js";
9
+ import { abortable } from "../cancellation.js";
10
+ import * as errors from "../errors.js";
11
+ import { TimestampTrailer } from "./timestamps.js";
12
+ export const transport = {
13
+ makeClient: (target) => new grpc.Client(target, grpc.credentials.createSsl(), {
14
+ "grpc.enable_retries": 0,
15
+ "grpc.max_receive_message_length": policy.receiveBytes,
16
+ "grpc.max_send_message_length": 131072,
17
+ }),
18
+ };
19
+ export function connect(client, signal) {
20
+ return connectGrpc(client, signal, policy.connectionTimeout);
21
+ }
22
+ function openStream(client, auth) {
23
+ return client.makeBidiStreamRequest("/rime.TextToSpeech/SynthesizeStreaming", serializer(schema.StreamingSynthesisRequestSchema), deserializer(schema.SynthesisResponseStreamSchema), auth);
24
+ }
25
+ function header(voice, language, timestamps, customLexicon, audioFormat, samplingRate, timeScaleFactor) {
26
+ return create(schema.StreamingSynthesisRequestSchema, {
27
+ payload: {
28
+ case: "header",
29
+ value: create(schema.SynthesisRequestSchema, {
30
+ speaker: voice,
31
+ language,
32
+ splitStrategy: schema.SplitStrategy.ACCUMULATE_SENTENCE,
33
+ audioParameters: { audioFormat, samplingRate, timeScaleFactor },
34
+ timestamps: timestamps ? { enable: true } : undefined,
35
+ customLexicon: [...customLexicon],
36
+ }),
37
+ },
38
+ });
39
+ }
40
+ function textMessage(text) {
41
+ return create(schema.StreamingSynthesisRequestSchema, {
42
+ payload: { case: "textChunk", value: text },
43
+ });
44
+ }
45
+ async function unary(prepared, name, input, output, request, signal) {
46
+ let call;
47
+ let id = null;
48
+ try {
49
+ signal.throwIfAborted();
50
+ const operation = new Promise((resolve, reject) => {
51
+ call = prepared.client.makeUnaryRequest("/rime.TextToSpeech/" + name, serializer(input), deserializer(output), request, prepared.metadata, (error, result) => (error ? reject(error) : resolve(result)));
52
+ call.once("metadata", (metadata) => {
53
+ id = requestId(metadata);
54
+ });
55
+ });
56
+ return await abortable(operation, signal);
57
+ }
58
+ catch (error) {
59
+ call?.cancel();
60
+ if (signal.aborted) {
61
+ if (signal.reason instanceof errors.RimeTimeoutError)
62
+ throw new errors.RimeTimeoutError(signal.reason.message, id, {
63
+ cause: signal.reason.cause,
64
+ });
65
+ throw signal.reason;
66
+ }
67
+ throw nativeError(error, id);
68
+ }
69
+ }
70
+ export async function discover(prepared, kind, language, signal) {
71
+ // Exactly one attempt. The client owns retries and the shared deadline.
72
+ if (kind === "voices") {
73
+ const method = schema.TextToSpeech.method.getSupportedSpeakers;
74
+ const response = await unary(prepared, method.name, method.input, method.output, create(method.input, { language: language ?? undefined }), signal);
75
+ return response.speakers;
76
+ }
77
+ const method = schema.TextToSpeech.method.getSupportedLanguages;
78
+ const response = await unary(prepared, method.name, method.input, method.output, create(method.input), signal);
79
+ return response.languages;
80
+ }
81
+ export class SynthesisCall {
82
+ audioFormat = "audio/pcm";
83
+ timestampsRequested = false;
84
+ timestampTrailer = new TimestampTrailer();
85
+ rpc;
86
+ constructor(prepared, signal) {
87
+ this.rpc = new BidiCall(openStream(prepared.client, prepared.metadata), signal);
88
+ }
89
+ get requestId() {
90
+ return this.rpc.requestId;
91
+ }
92
+ get done() {
93
+ return this.rpc.done;
94
+ }
95
+ cancel() {
96
+ this.rpc.cancel();
97
+ }
98
+ start(voice, language, timestamps = false, customLexicon = [], audioFormat = "audio/pcm", sampleRate = 24000, timeScaleFactor) {
99
+ this.timestampsRequested = timestamps;
100
+ this.audioFormat = audioFormat;
101
+ const message = header(voice, language, timestamps, customLexicon, audioFormat, sampleRate, timeScaleFactor);
102
+ return this.rpc.write(message);
103
+ }
104
+ timestampResult() {
105
+ return this.timestampTrailer.result(this.requestId);
106
+ }
107
+ write(text) {
108
+ return this.rpc.write(textMessage(text));
109
+ }
110
+ finishInput() {
111
+ this.rpc.finishInput();
112
+ }
113
+ checkFormat(contentType) {
114
+ if (contentType !== this.audioFormat)
115
+ throw new errors.RimeAudioFormatError(`Expected ${this.audioFormat} from the service`, this.requestId);
116
+ }
117
+ async *audio() {
118
+ const headers = await this.rpc.headers();
119
+ const contentType = headers.get("x-rime-audio-content-type")[0];
120
+ for await (const response of this.rpc.responses()) {
121
+ if (response.payload.case === "trailer" && this.timestampsRequested)
122
+ this.timestampTrailer.accept(response.payload.value);
123
+ if (response.payload.case !== "audio")
124
+ continue;
125
+ if (this.timestampTrailer.seen)
126
+ this.timestampTrailer.invalid = true;
127
+ if (response.payload.value.length)
128
+ this.checkFormat(contentType);
129
+ yield response.payload.value;
130
+ }
131
+ this.checkFormat(contentType);
132
+ }
133
+ }
@@ -0,0 +1,5 @@
1
+ export declare function endpointAddress(endpoint: string): {
2
+ target: string;
3
+ audience: string;
4
+ };
5
+ export declare function timeout(value: unknown, inherited?: number | null): number | null;
@@ -0,0 +1,28 @@
1
+ import { RimeInputError } from "./errors.js";
2
+ export function endpointAddress(endpoint) {
3
+ const error = "endpoint must be a hostname with an optional port (1-65535), without a scheme or path";
4
+ if (typeof endpoint !== "string" || /\s/.test(endpoint))
5
+ throw new RimeInputError(error);
6
+ const [host, port, extra] = endpoint.split(":");
7
+ if (!host ||
8
+ host.length > 253 ||
9
+ extra !== undefined ||
10
+ host
11
+ .split(".")
12
+ .some((label) => !/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i.test(label)) ||
13
+ (port !== undefined &&
14
+ (!/^[0-9]{1,5}$/.test(port) || Number(port) < 1 || Number(port) > 65535)))
15
+ throw new RimeInputError(error);
16
+ return {
17
+ target: `${host.toLowerCase()}:${port === undefined ? 443 : Number(port)}`,
18
+ audience: host.toLowerCase(),
19
+ };
20
+ }
21
+ export function timeout(value, inherited = null) {
22
+ if (value === undefined)
23
+ return inherited;
24
+ if (value !== null &&
25
+ (typeof value !== "number" || !Number.isFinite(value) || value <= 0))
26
+ throw new RimeInputError("timeout must be finite positive seconds or null");
27
+ return value;
28
+ }
@@ -0,0 +1,426 @@
1
+ # Realtime / Prism for Node.js
2
+
3
+ The endpoint must use Rime's `prism.` events and `prism_` fields. Endpoints
4
+ that use the former `prsm` names are not supported.
5
+
6
+ Keep one event consumer running while your application sends input, runs tools,
7
+ and plays audio. The SDK manages the protocol state. Your application owns
8
+ microphone capture, tool execution, and playback.
9
+
10
+ Start with the [recorded-speech quick start](../README.md#prism-send-recorded-speech)
11
+ to save a spoken reply without audio hardware.
12
+
13
+ For live conversations, keep the session open after a response is cancelled.
14
+ Report playback from the output clock. Saving a file is not playback.
15
+ Typed turns also support mixed text and voice applications.
16
+
17
+ ## Optional conversation runner
18
+
19
+ Use `runConversation` to connect your audio adapters once. It sends input,
20
+ queues output, handles interruption, reports measured playback, and stops its
21
+ operations when the conversation ends. The direct session methods remain available.
22
+
23
+ ```ts
24
+ import { Rime, runConversation } from "@rimelabs/sdk";
25
+
26
+ const client = new Rime();
27
+ const controller = new AbortController();
28
+ try {
29
+ const session = await client.realtime.connect();
30
+ // microphone and speaker are adapters owned by your application.
31
+ await runConversation(session, {
32
+ source: microphone,
33
+ playback: speaker,
34
+ onEvent(event) {
35
+ if (event.payload.kind === "transcript.final")
36
+ console.log(event.payload.text);
37
+ },
38
+ signal: controller.signal,
39
+ });
40
+ } finally {
41
+ await client.close();
42
+ }
43
+ ```
44
+
45
+ The input adapter implements `ConversationSource`.
46
+ `read(signal: AbortSignal): Promise<AudioChunk | null>` returns one chunk,
47
+ with an optional format, or `null` at EOF. An omitted format uses 16 kHz mono
48
+ PCM16. Abort must reject and release a pending
49
+ read. The runner awaits each `session.sendAudio` call before reading again. It
50
+ uses the session's existing PCM conversion and transport backpressure. Input
51
+ continues during output playback.
52
+
53
+ The playback adapter implements `ConversationPlayback`:
54
+
55
+ | Method | Contract |
56
+ | --- | --- |
57
+ | `open(output: OutputRef): ConversationOutput` | Queue this output after previously opened outputs. It can be opened before the first audio chunk. |
58
+ | `interrupt(outputs: readonly OutputRef[]): void` | Stop the selected outputs as one batch, discard their unplayed audio, then resume remaining output in queue order. |
59
+ | `stop(signal: AbortSignal): Promise<void>` | Confirm all playback has stopped and queued audio is discarded. Keep caller-owned resources open. |
60
+
61
+ Each `ConversationOutput` implements:
62
+
63
+ | Method | Contract |
64
+ | --- | --- |
65
+ | `write(chunk: AudioChunk): void` | Queue 24 kHz mono PCM16 without blocking. Bound the queue and throw on overflow. |
66
+ | `end(): void` | Accept no more audio. Let queued and device audio drain. |
67
+ | `completion(signal: AbortSignal): Promise<number>` | Return actual played milliseconds after drain or interruption. Reject if measurement fails or the signal aborts. |
68
+
69
+ All synchronous adapter methods must return promptly. Completion can be awaited
70
+ before `end()`. Keep receipts separate for each `OutputRef`, even when a new
71
+ response starts before an old receipt completes. Play outputs in the order they
72
+ were opened, using one FIFO audio queue. Do not mix outputs, interleave their
73
+ audio, or play later outputs before earlier outputs finish or are interrupted.
74
+ Prism assumes that interrupting a partially played reply leaves later replies
75
+ in the same turn unheard. Independent concurrent playback violates that assumption.
76
+ The runner passes all affected output references in one `interrupt` call. Stop
77
+ them and discard their queued audio before starting any remaining output. Then
78
+ resume valid output in queue order, without waiting for another chunk or event.
79
+ Interruption must work after `end()` and tolerate repeated calls. A remote
80
+ adapter must preserve the batch, for example with one message containing all
81
+ selected output IDs. Shutdown stops all output and must not resume playback.
82
+ Receipt delivery can be delayed or arrive out of order; audible playback cannot.
83
+ A receipt includes the device buffer where device timing is available. Bytes
84
+ received or queued do not measure playback. Zero is valid for an output that
85
+ played no audio.
86
+
87
+ Abort must release each pending adapter operation and remove its listeners.
88
+ Cancellation of a completion wait must not cancel the device receipt. The runner
89
+ stops input, event processing, and report submission on exit. It then awaits
90
+ `stop(signal)` before aborting remaining receipt waits. The stop signal is
91
+ independent of caller cancellation and has a five-second deadline. `stop` must
92
+ resolve only after local or remote playback has stopped, including device
93
+ buffers. Sending a stop command is not confirmation. It must reject and release
94
+ its wait when the stop signal aborts. The runner awaits this cancellation cleanup
95
+ before it returns, so cleanup can extend beyond the deadline. A missing confirmation raises
96
+ `RimeTimeoutError`; disconnects and other shutdown failures must reject too.
97
+ The caller opens and closes the adapter resources. The runner does not open
98
+ audio devices or host a browser.
99
+
100
+ `onEvent` is optional. It receives every event read from `session.events`, in
101
+ order, after the runner applies audio coordination. It must be synchronous and
102
+ nonblocking. Returning a promise is rejected. Callback exceptions reach the
103
+ caller. The callback can schedule application work. Continue to use
104
+ `submitToolResult` and `continueReply`; the runner neither registers nor executes
105
+ tools. The application must cancel and await its own tool tasks. Response
106
+ failures and nonterminal faults are forwarded for application handling. Session
107
+ failures reject the runner promise.
108
+
109
+ The runner is the only event reader. Do not consume `session.events` at the same
110
+ time, call `reportPlayback` independently, or send a second audio source. Other
111
+ session methods remain available for manual tools and application actions.
112
+
113
+ On confirmed `speech.started`, the runner uses the effective
114
+ `session.info.interruptOnSpeech` setting. It stops affected output, waits for the
115
+ interrupted receipt, and reports the measured position. It ignores late audio
116
+ for that response. A `speech.hint` alone does not stop playback. A server
117
+ `truncate_not_current` refusal is forwarded as an event; it does not stop a newer
118
+ response. Finished reports wait for generation completion and every output
119
+ receipt, including outstanding playback from earlier responses. Prism treats
120
+ the latest drain token as confirmation that all earlier playback has stopped.
121
+ An abandoned response also stops its dependent tool continuations, identified
122
+ by response parent references. Unrelated later turns remain eligible to play.
123
+ Late audio from an interrupted response or its continuations is discarded.
124
+ The runner retains at most 128 response parent references.
125
+ A report retains the measured duration of its own response. Waiting for earlier
126
+ playback does not block events or input, and does not wait for later responses.
127
+
128
+ Once called, the runner owns the session's active lifetime. Input EOF, explicit
129
+ session closure, abort, or failure stops all runner operations and closes the
130
+ session. EOF stops remaining playback and waits for shutdown confirmation.
131
+ Abort, including an abort received during cleanup, waits for playback shutdown
132
+ confirmation or its five-second deadline, then awaits adapter cancellation cleanup
133
+ before rejecting with `signal.reason`.
134
+ Explicit session closure ends normally even with playback reports pending;
135
+ remote connection failures reject with the session error.
136
+ Expected cancellation errors during cleanup are ignored. Other cleanup errors
137
+ reach the caller if there is no original failure. They do not replace an
138
+ existing failure or caller abort. The signal is optional. Use
139
+ an application deadline if an adapter can wait indefinitely.
140
+
141
+ The runner adds no input or callback queue. It retains at most 64 responses,
142
+ 128 output streams, and 256 pending operations. Exceeding a limit throws
143
+ `RimeResourceLimitError`. The adapters must also bound their audio queues.
144
+
145
+ The [live voice example](https://github.com/rimelabs/rime-sdk/blob/main/typescript/examples/realtime/conversation.ts) supplies the
146
+ [browser audio adapter](https://github.com/rimelabs/rime-sdk/blob/main/typescript/examples/common/browser-audio.ts). Browser hosting
147
+ stays in the example. Its player uses `AudioContext.getOutputTimestamp()` for
148
+ receipts and shutdown confirmation. A disconnect cannot confirm a remote stop.
149
+ The [manual tool example](https://github.com/rimelabs/rime-sdk/blob/main/typescript/examples/realtime/manual-tools.ts) keeps
150
+ tool execution outside the runner.
151
+
152
+ ## Connect
153
+
154
+ Use `await client.realtime.connect()` with `RIME_API_KEY` set. It opens a WebSocket,
155
+ initializes one conversation, and returns a `RealtimeSession`. Settings apply
156
+ once. Open a new session to change them.
157
+
158
+ | Option | Required? | Default and behavior |
159
+ | --- | --- | --- |
160
+ | `endpoint` | No | Defaults to `wss://prism.api.rime.ai/v1/realtime`. Overrides must be full URLs; plain `ws://` is allowed only for localhost. |
161
+ | `voice` | If the server has no default voice | Omitted or `null` uses the server default. Supplied strings must be nonblank. |
162
+ | `instructions` | No | Omitted or `null` uses server defaults. Supplied strings must be nonblank. |
163
+ | `tools` | No | `[]`. Each tool requires `name` and a JSON Schema `parameters` object. `description` defaults to `""`. |
164
+ | `interruptOnSpeech` | No | `true`. Requests interruption when the server confirms user speech. |
165
+ | `turnDetection` | No | Omitted settings use the server's turn timing defaults. |
166
+ | `timeouts` | No | `{ connectS: 10, readyS: 30, requestS: 10 }`. Each field is optional. Units are seconds. |
167
+ | `model` | No | `"prism"`, the only accepted value. |
168
+ | `signal` | No | An `AbortSignal` that cancels connection setup. It does not cancel an established session. |
169
+
170
+ Credentials come from `new Rime({ apiKey: "..." })` or `RIME_API_KEY`. Prism uses
171
+ `Authorization: Bearer <key>`, without a TTS token exchange. TTS endpoint, model,
172
+ voice, and timeout settings do not apply to Realtime.
173
+
174
+ After connection, `session.info` contains `sessionId`, `model`, `voice`,
175
+ `interruptOnSpeech`, `turnDetection`, `toolResultTimeoutS`, and
176
+ `toolContinuationTimeoutS`.
177
+ Check the effective interruption setting there. Tool wait durations are total
178
+ server deadlines, not remaining time. A `null` duration can mean an omitted
179
+ field; it does not prove that no deadline exists.
180
+
181
+ Close the session in `finally` with `await session.close()`. Closing the parent
182
+ client closes all its TTS and Realtime operations. Both objects also support
183
+ `Symbol.asyncDispose`. Repeated close calls are safe.
184
+
185
+ ## Optional turn timing
186
+
187
+ Pass a `TurnDetection` object to override how readily Prism ends a spoken turn.
188
+ Each field is optional; omitted fields use server defaults.
189
+
190
+ | Field | Values | Current server default |
191
+ | --- | --- | --- |
192
+ | `eagerness` | `"low"`, `"medium"`, `"high"`, `"auto"` | `"auto"`, equivalent to `"medium"` |
193
+ | `endOfTurnTimeoutMs` | Integer from 1,000 through 10,000 | 5,000 ms |
194
+
195
+ `"low"` waits for more certainty that the user has finished. `"high"` answers
196
+ sooner, with more risk of cutting in. The timeout is a maximum silence limit,
197
+ not a fixed delay before each reply. If the server falls back to its local
198
+ voice activity detector, it ends turns after 500 ms of silence instead.
199
+
200
+ ```typescript
201
+ const session = await client.realtime.connect({
202
+ turnDetection: { eagerness: "low", endOfTurnTimeoutMs: 5000 },
203
+ });
204
+ try {
205
+ console.log(session.info.turnDetection);
206
+ // Send input and consume session.events here.
207
+ } finally {
208
+ await session.close();
209
+ }
210
+ ```
211
+
212
+ `session.info.turnDetection` is a `TurnDetectionInfo` with the settings the
213
+ server reported in its initialization acknowledgment. A missing field is
214
+ `null`; the SDK does not substitute a requested value or assume a server
215
+ default. An unrecognized server eagerness becomes `"unknown"`.
216
+ `interruptOnSpeech` remains a separate connection option.
217
+
218
+ ## Choose an operation
219
+
220
+ Every method returns a promise. Arguments without `?` are required.
221
+
222
+ | Method | Result and behavior |
223
+ | --- | --- |
224
+ | `sendText(text, options?)` | Starts a typed user turn after readiness; returns `ResponseRef` at response creation. |
225
+ | `sendAudio(chunk, options?)` | Sends microphone audio with transport backpressure. |
226
+ | `requestReply(options?)` | Requests a reply without new user input; returns `ResponseRef`. Options can include `instruction` and `toolCall`. |
227
+ | `addMessage(role, text, options?)` | Adds user or assistant history without a reply; returns `ItemRef` after acknowledgment. |
228
+ | `submitToolResult(call, output, options?)` | Records a string result and waits for acknowledgment. |
229
+ | `continueReply(parent, options?)` | Waits for parent completion and all tool result acknowledgments, then returns a new `ResponseRef`. |
230
+ | `cancel(response, options?)` | Requests generation cancellation and waits for a terminal response or refusal. |
231
+ | `clearAudio(options?)` | Clears buffered input and resets audio conversion after acknowledgment. |
232
+ | `reportPlayback(report, options?)` | Reports interrupted or finished playback. |
233
+ | `close()` | Closes the session and releases its resources. |
234
+
235
+ Operation options accept `signal?: AbortSignal`. `requestReply` accepts its
236
+ instruction, tool call, and signal in one object. Methods other than `sendText`,
237
+ `requestReply`, `addMessage`, and `continueReply` return `Promise<void>`.
238
+
239
+ Text must be nonblank and contain at most 4,000 Unicode characters. History
240
+ roles are `"user"` and `"assistant"`. A reply instruction applies to one response;
241
+ it does not replace session instructions.
242
+
243
+ A returned `ResponseRef` means generation started. Read `response.ended` for the
244
+ terminal status. Generation can finish before cancellation takes effect, so a
245
+ successful `cancel()` does not guarantee a `"cancelled"` status.
246
+
247
+ ## Send audio
248
+
249
+ Pass a plain object with `data: Uint8Array`. Node.js `Buffer` is also accepted.
250
+ The optional `format` object defaults to 16 kHz mono signed little-endian PCM16.
251
+
252
+ ```typescript
253
+ await session.sendAudio({
254
+ data: pcmBytes,
255
+ format: { sampleRate: 48000, channels: 2 },
256
+ });
257
+ ```
258
+
259
+ Supported `sampleRate` values are `8000`, `16000`, `24000`, and `48000`.
260
+ Channels can be `1` or `2`; `encoding` can only be `"pcm_s16le"`. Each field in
261
+ `format` is optional. Chunks must contain complete sample frames and at most
262
+ 192,000 bytes. The SDK downmixes and resamples to 16 kHz mono, retaining state
263
+ across calls. A format change or acknowledged `clearAudio()` resets that state.
264
+
265
+ Await each send and bound your capture queue. Keep audio flowing, including
266
+ silence and while the assistant speaks or tools run. Prism detects and commits
267
+ speech; there is no manual commit method. Echo cancellation belongs to your app.
268
+
269
+ Received `audio.delta` events carry 24 kHz mono PCM16. The outgoing chunk limit
270
+ does not apply to received audio.
271
+
272
+ ## Handle tools
273
+
274
+ Declare tools with plain `ToolDefinition` objects. Names must be nonblank and
275
+ unique; schemas must contain finite JSON values. The SDK copies schemas before
276
+ connection setup, so later caller changes cannot alter initialization.
277
+
278
+ Set `prismFiller` to control what Prism says while the tool runs:
279
+
280
+ ```ts
281
+ import type { ToolDefinition } from "@rimelabs/sdk";
282
+
283
+ const pressDigits: ToolDefinition = {
284
+ name: "press_digits",
285
+ parameters: { type: "object", properties: { digits: { type: "string" } } },
286
+ prismFiller: { type: "none" },
287
+ };
288
+ ```
289
+
290
+ | Setting | Behavior |
291
+ | --- | --- |
292
+ | Omitted | Use the server default, currently `auto`. |
293
+ | `{ type: "none" }` | Suppress filler for this tool. |
294
+ | `{ type: "text", text: "One moment." }` | Speak the supplied text. |
295
+ | `{ type: "auto" }` | Let Prism write a line from the tool description. |
296
+ | `{ type: "auto", instructions: "Say you are checking the order." }` | Guide the generated line. |
297
+
298
+ Text and instructions must be valid UTF-8, contain at most 500 Unicode scalar
299
+ values, and contain a character outside the Unicode `White_Space` set. This
300
+ matches the engine's validation. The SDK preserves the supplied text and copies
301
+ filler settings before connection setup. Invalid settings raise `RimeInputError` before opening a
302
+ connection, including for JavaScript callers. The exported `PrismFiller` union
303
+ also checks TypeScript callers.
304
+
305
+ Filler applies when the reply has said nothing before calling a tool. If a reply
306
+ calls several tools, the first call's tool controls the filler. `none` suppresses
307
+ filler only; it does not silence other speech. Settings are fixed for the session.
308
+
309
+ Execute a tool once for each `tool.call` event. Validate its parsed `arguments`,
310
+ then submit a string result with `submitToolResult(payload.call, output)`.
311
+ Use `JSON.stringify(...)` for a structured result. Call
312
+ `continueReply(payload.call)` when the assistant should answer; a tool call
313
+ reference also carries its parent response identity.
314
+
315
+ Keep the event consumer running while submitting tool results and requesting
316
+ continuation. Run slow tools asynchronously or in a worker so they do not block
317
+ the event loop.
318
+
319
+ A new user turn or proactive reply supersedes the old tool round. Late results
320
+ can still be recorded. Use `requestReply({ toolCall: call })` to ask the assistant
321
+ to report a recorded result. Repeating the same result is safe within the live
322
+ session; changing it is rejected. Do not rerun a tool because delivery failed.
323
+
324
+ ## Read events and references
325
+
326
+ Use `for await (const event of session.events)`. Each `SessionEvent` has
327
+ `sessionId`, `eventId`, nullable `requestId`, and a `payload`. Switch on
328
+ `payload.kind` to narrow the TypeScript union. All public types and error classes
329
+ are exported from `@rimelabs/sdk`.
330
+
331
+ | Kind | Payload fields |
332
+ | --- | --- |
333
+ | `input.ready` | No data fields. Turn methods handle the readiness wait. |
334
+ | `speech.hint` | `audioStartMs`; the user may have started speaking over a playing reply. It can be a false alarm: lower playback, do not stop it. |
335
+ | `speech.started` | `itemId`, `audioStartMs` |
336
+ | `speech.stopped` | `itemId`, `audioEndMs`, `endReason`: what ended the turn (`semantic`, `timeout`, `silence`, `max_duration` or `detector_changed`), `unknown` for a newer value, or null from a server that does not send it |
337
+ | `input.committed` | `itemId` |
338
+ | `transcript.delta`, `transcript.final` | `itemId` and `delta` or `text` |
339
+ | `transcript.failed` | `itemId`, `error` |
340
+ | `response.started` | `response`, `cause`, nullable `parent` and `inputItemId` |
341
+ | `message.started` | `output` |
342
+ | `text.delta`, `text.done` | `output` and `delta` or `text` |
343
+ | `audio.delta`, `audio.done` | `output`; a delta also has `audio.data` and a complete `audio.format` |
344
+ | `tool.call` | `call`, `itemId`, `name`, parsed `arguments` |
345
+ | `response.ended` | `response`, `status`, nullable `reason` |
346
+ | `response.abandoned` | `response`; the SDK requests cancellation after the requesting caller aborted. |
347
+ | `error` | `error`, a `RealtimeFault` |
348
+
349
+ References come from the SDK. Pass them back to identify the object you mean.
350
+
351
+ | Reference | Fields |
352
+ | --- | --- |
353
+ | `ResponseRef` | `sessionId`, `responseId` |
354
+ | `ItemRef` | `sessionId`, `itemId` |
355
+ | `ToolCallRef` | `sessionId`, `responseId`, `callId` |
356
+ | `OutputRef` | `response`, `itemId`, `outputIndex`, `contentIndex` |
357
+
358
+ Every reference belongs to one session. Only the last 128 responses are retained;
359
+ older references expire. Current output uses `contentIndex: 0`.
360
+
361
+ Unknown event kinds and extra fields are ignored. Unknown response causes,
362
+ terminal statuses, and error scopes become `"unknown"`. An absent response cause
363
+ means speech. The local `response.abandoned` event has an empty `eventId` and
364
+ `requestId: null`; it can follow `response.ended` when completion races cancellation.
365
+
366
+ ## Report playback
367
+
368
+ Generation completion and playback completion are separate. Report the player's
369
+ actual state, including output latency and queued audio.
370
+
371
+ | Report object | Required fields | Optional fields |
372
+ | --- | --- | --- |
373
+ | Interrupted | `kind: "interrupted"`, `output`, `audioEndMs` | None |
374
+ | Finished | `kind: "finished"`, `response` | `playedMs`, `audibleTailMs`; both default to `null` |
375
+
376
+ Pass the object to `await session.reportPlayback(report)`. Interruption records
377
+ the point where caller speech stopped an assistant message. `audioEndMs` must
378
+ be an integer from 0 through 4,294,967,295. Finished playback means all queued
379
+ audio has played or has permanently stopped. Optional durations must be finite,
380
+ nonnegative milliseconds. The SDK waits for generation completion and supplies
381
+ the server's drain token.
382
+
383
+ Saving audio does not establish playback. Reports are optional. If you cannot
384
+ measure playback, omit them and let the server use its estimate. The recorded and typed examples have no player and omit these reports.
385
+ The voice examples report actual playback.
386
+
387
+ ## Timeouts and failures
388
+
389
+ Configure positive, finite seconds through `timeouts`. `connectS` bounds the
390
+ WebSocket handshake and initial session event. `readyS` bounds turn admission,
391
+ tool readiness, and waiting for generation to end before a playback report.
392
+ Once a request is submitted, `requestS` bounds its write and acknowledgment.
393
+
394
+ These limits do not bound the entire conversation or generation after creation.
395
+ Set an application deadline and close the session when it expires. The recorded and typed examples use a 60-second deadline. Voice conversations
396
+ run until stopped or the connection fails.
397
+
398
+ `RealtimeAdmissionTimeout` means readiness expired with no request outstanding;
399
+ the session remains usable. An unknown request outcome closes the connection and
400
+ is never replayed. The SDK retries typed-turn admission only after named refusals
401
+ that confirm the request made no change.
402
+
403
+ Protocol faults appear as `error` events and can also reject an affected method
404
+ with `RimeRealtimeError`. Its `.fault` has `code`, `message`, `scope`, nullable
405
+ `requestId`, `responseId`, `itemId`, `correlationId`, `parameter`, and a `callIds`
406
+ array. A response fault does not itself end generation. Session faults and lost
407
+ connections end the session.
408
+
409
+ Malformed fields in known events and mismatched request acknowledgments close
410
+ the session with `RimeStreamError`. Unknown event kinds and extra fields remain
411
+ accepted and ignored. The SDK does not replay a request after a protocol failure.
412
+
413
+ Aborting an operation rejects it with `RimeCancelledError`. For reply requests,
414
+ the SDK prevents an unsent request or cancels a late accepted response. Control
415
+ operations can wait for acknowledgment before rejecting, to keep acknowledgment
416
+ matching correct. A submitted tool result keeps matching its acknowledgment even
417
+ if its caller aborts. Aborting audio after submission starts closes the session;
418
+ before submission, it restores conversion state. Do not resend a chunk with an
419
+ unknown outcome.
420
+
421
+ Keep up with the event stream. The queue holds 256 events; incoming messages
422
+ are limited to 1 MiB. A slow consumer ends the session. There is no automatic
423
+ reconnect, history replay, or live configuration update.
424
+
425
+ Local peer tests cover the SDK protocol handling. Validate the gateway handshake
426
+ and endpoint behavior for the deployment you use.