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

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.0-alpha.21](https://github.com/rimelabs/rime-sdk/compare/typescript-v0.1.0-alpha.20...typescript-v0.1.0-alpha.21) (2026-10-23)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * Rust RealtimeSession::info returns an owned SessionInfo snapshot instead of a reference so cloned handles can read current settings.
9
+
10
+ ### Features
11
+
12
+ * expose Prism error retry classification ([#99](https://github.com/rimelabs/rime-sdk/issues/99)) ([c04309c](https://github.com/rimelabs/rime-sdk/commit/c04309c174c2eed82df3b676581c45cab2a95db6))
13
+ * **realtime:** request ids, reply labels, and the items a reply answers ([#95](https://github.com/rimelabs/rime-sdk/issues/95)) ([f33950b](https://github.com/rimelabs/rime-sdk/commit/f33950b2d0fdb960f4701c1a1ec208c0d539d5ec))
14
+ * support mid-session Prism settings updates in all SDKs ([#101](https://github.com/rimelabs/rime-sdk/issues/101)) ([83f5347](https://github.com/rimelabs/rime-sdk/commit/83f534761418217292fbbce8dbdc47454ad5ddb8))
15
+
3
16
  ## [0.1.0-alpha.20](https://github.com/rimelabs/rime-sdk/compare/typescript-v0.1.0-alpha.19...typescript-v0.1.0-alpha.20) (2026-10-22)
4
17
 
5
18
 
package/README.md CHANGED
@@ -324,6 +324,7 @@ See the [TTS guide](https://github.com/rimelabs/rime-sdk/blob/main/typescript/do
324
324
  for more examples. Visit [Rime documentation](https://docs.rime.ai/) for service
325
325
  information, or report SDK issues on [GitHub](https://github.com/rimelabs/rime-sdk/issues).
326
326
  Include the SDK version and the error's `requestId` when available; omit API keys.
327
+ For Prism, include `session.info.requestId`.
327
328
 
328
329
  SDK code uses the [MIT license](LICENSE). The Prism protocol module uses
329
330
  [Apache 2.0](LICENSE-PRISM); see its [notices](NOTICE-PRISM).
@@ -3,6 +3,8 @@ import { RimeCancelledError, RimeInputError, RimeUnavailableError, } from "../er
3
3
  import { cancellation, scope } from "./async.js";
4
4
  import { RealtimeSession } from "./session.js";
5
5
  import { RealtimeConnection } from "./transport.js";
6
+ import { toolSettings } from "./settings.js";
7
+ import { responseMetadata } from "./protocol.js";
6
8
  const DEFAULT_ENDPOINT = "wss://prism.api.rime.ai/v1/realtime";
7
9
  function endpoint(value) {
8
10
  let url;
@@ -34,48 +36,6 @@ function timeouts(options = {}) {
34
36
  throw new RimeInputError("Timeouts must be finite and positive");
35
37
  return result;
36
38
  }
37
- /** Reject values that JSON.stringify would silently drop or change. */
38
- function copyJson(value) {
39
- if (value === null || typeof value === "string" || typeof value === "boolean")
40
- return value;
41
- if (typeof value === "number" && Number.isFinite(value))
42
- return value;
43
- if (Array.isArray(value))
44
- return Array.from(value, copyJson);
45
- if (value &&
46
- typeof value === "object" &&
47
- [Object.prototype, null].includes(Object.getPrototypeOf(value)))
48
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, copyJson(item)]));
49
- throw new RimeInputError("Tool schemas must contain finite JSON values");
50
- }
51
- function fillerSettings(value) {
52
- if (!value || typeof value !== "object" || Array.isArray(value))
53
- throw new RimeInputError("prismFiller must be a filler object");
54
- const kind = value.type;
55
- if (!["none", "text", "auto"].includes(kind))
56
- throw new RimeInputError("Prism filler type must be none, text, or auto");
57
- const field = kind === "text" ? "text" : kind === "auto" ? "instructions" : null;
58
- if (Object.entries(value).some(([key, item]) => item !== undefined && key !== "type" && key !== field))
59
- throw new RimeInputError("Prism filler has fields not allowed for its type");
60
- const text = field === null ? undefined : value[field];
61
- if (kind === "text" || text !== undefined) {
62
- // Rust str::trim uses Unicode White_Space, which differs from JS trim.
63
- if (typeof text !== "string" ||
64
- !/\P{White_Space}/u.test(text) ||
65
- [...text].length > 500)
66
- throw new RimeInputError("Prism filler text must be nonblank and at most 500 characters");
67
- // Unicode mode matches lone surrogates but preserves valid UTF-16 pairs.
68
- if (/[\uD800-\uDFFF]/u.test(text))
69
- throw new RimeInputError("Prism filler text must be valid UTF-8");
70
- }
71
- if (kind === "text")
72
- return { type: "text", text: text };
73
- if (kind === "auto")
74
- return text === undefined
75
- ? { type: "auto" }
76
- : { type: "auto", instructions: text };
77
- return { type: "none" };
78
- }
79
39
  export class Realtime {
80
40
  credentials;
81
41
  checkOpen;
@@ -94,46 +54,11 @@ export class Realtime {
94
54
  if (options.interruptOnSpeech !== undefined &&
95
55
  typeof options.interruptOnSpeech !== "boolean")
96
56
  throw new RimeInputError("interruptOnSpeech must be a boolean");
97
- const names = new Set();
98
57
  const settings = {
99
58
  modalities: ["text", "audio"],
100
59
  input_audio_format: "pcm16",
101
60
  turn_detection: { interrupt_response: options.interruptOnSpeech ?? true },
102
- tools: (options.tools ?? []).map((tool) => {
103
- if (typeof tool.name !== "string" ||
104
- !tool.name.trim() ||
105
- names.has(tool.name))
106
- throw new RimeInputError("Tool names must be nonblank and unique");
107
- names.add(tool.name);
108
- if (!tool.parameters ||
109
- Array.isArray(tool.parameters) ||
110
- typeof tool.parameters !== "object" ||
111
- (tool.description !== undefined &&
112
- typeof tool.description !== "string"))
113
- throw new RimeInputError("Tool parameters must be a JSON object and description must be a string");
114
- let parameters;
115
- try {
116
- parameters = copyJson(tool.parameters);
117
- }
118
- catch {
119
- throw new RimeInputError("Tool schemas must contain finite JSON values");
120
- }
121
- if (parameters === null ||
122
- typeof parameters !== "object" ||
123
- Array.isArray(parameters))
124
- throw new RimeInputError("Tool parameters must be an object");
125
- return {
126
- type: "function",
127
- ...(tool.prismFiller === undefined
128
- ? {}
129
- : { prism_filler: fillerSettings(tool.prismFiller) }),
130
- function: {
131
- name: tool.name,
132
- description: tool.description ?? "",
133
- parameters,
134
- },
135
- };
136
- }),
61
+ tools: toolSettings(options.tools ?? []),
137
62
  };
138
63
  if (options.turnDetection !== undefined) {
139
64
  const detection = options.turnDetection;
@@ -153,6 +78,14 @@ export class Realtime {
153
78
  settings.turn_detection.prism_end_of_turn_timeout_ms = value;
154
79
  }
155
80
  }
81
+ if (options.responseMetadata != null)
82
+ settings.prism_response_metadata = responseMetadata(options.responseMetadata);
83
+ const requestId = options.requestId ?? undefined;
84
+ if (requestId !== undefined &&
85
+ (typeof requestId !== "string" ||
86
+ !requestId.trim() ||
87
+ !/^[\x20-\x7e]+$/.test(requestId)))
88
+ throw new RimeInputError("requestId must be nonblank printable ASCII");
156
89
  for (const name of ["voice", "instructions"]) {
157
90
  const value = options[name];
158
91
  if (value !== undefined && value !== null) {
@@ -170,7 +103,7 @@ export class Realtime {
170
103
  openingScope.signal.throwIfAborted();
171
104
  let socket;
172
105
  try {
173
- socket = new RealtimeConnection(address, this.credentials.realtimeAuthorization());
106
+ socket = new RealtimeConnection(address, this.credentials.realtimeAuthorization(), requestId);
174
107
  }
175
108
  catch {
176
109
  throw new RimeUnavailableError("Realtime handshake failed");
@@ -1,5 +1,5 @@
1
1
  import type { RealtimeSession } from "./session.js";
2
- import type { AudioChunk, OutputRef, SessionEvent } from "./types.js";
2
+ import type { AudioChunk, NormalizedRealtimeFault, OutputRef, SessionEvent } from "./types.js";
3
3
  export interface ConversationSource {
4
4
  /** Return one chunk, or null at EOF. Abort must release a pending read. */
5
5
  read(signal: AbortSignal): Promise<AudioChunk | null>;
@@ -34,7 +34,7 @@ export interface ConversationOptions {
34
34
  readonly playback: ConversationPlayback;
35
35
  /** Called inline for every event. Must be synchronous and nonblocking.
36
36
  * Scheduled application work remains the caller's responsibility. */
37
- readonly onEvent?: (event: SessionEvent) => void;
37
+ readonly onEvent?: (event: SessionEvent<NormalizedRealtimeFault>) => void;
38
38
  readonly signal?: AbortSignal;
39
39
  }
40
40
  /** Run until input EOF, session closure, failure, or abort.
@@ -1,7 +1,7 @@
1
1
  import { abortable, scope } from "../cancellation.js";
2
2
  import { RimeCancelledError, RimeInputError, RimeResourceLimitError, RimeTimeoutError, } from "../errors.js";
3
3
  import { Deferred } from "./async.js";
4
- import { isSessionCloseError } from "./session.js";
4
+ import { isSessionCloseError, responseInterruptsSpeech } from "./session.js";
5
5
  import { RimeRealtimeError } from "./types.js";
6
6
  /** Run until input EOF, session closure, failure, or abort.
7
7
  * Owns the session's active lifetime and closes it on exit. The caller owns
@@ -171,7 +171,12 @@ export async function runConversation(session, { source, playback, onEvent, sign
171
171
  stream,
172
172
  receipt: start(() => measure(stream)),
173
173
  sealed: false,
174
+ measured: false,
174
175
  };
176
+ const measuredOutput = output;
177
+ void output.receipt.then(() => {
178
+ measuredOutput.measured = true;
179
+ }, () => { });
175
180
  reply.outputs.set(key, output);
176
181
  if (reply.interrupted) {
177
182
  playback.interrupt([ref]);
@@ -209,6 +214,7 @@ export async function runConversation(session, { source, playback, onEvent, sign
209
214
  interrupted.add(id);
210
215
  replies.set(id, {
211
216
  ref: payload.response,
217
+ interruptOnSpeech: responseInterruptsSpeech(session, payload.response),
212
218
  outputs: new Map(),
213
219
  interrupted: blocked,
214
220
  ended: false,
@@ -229,10 +235,16 @@ export async function runConversation(session, { source, playback, onEvent, sign
229
235
  }
230
236
  break;
231
237
  }
232
- case "speech.started":
233
- if (session.info.interruptOnSpeech)
234
- interrupt([...replies.values()]);
238
+ case "speech.started": {
239
+ // The newest eligible reply controls the playback batch.
240
+ // A pending finished report is no longer physical playback.
241
+ const eligible = [...replies.values()].filter((reply) => !reply.interrupted &&
242
+ (!reply.ended ||
243
+ [...reply.outputs.values()].some((output) => !output.measured)));
244
+ if (eligible.at(-1)?.interruptOnSpeech)
245
+ interrupt(eligible);
235
246
  break;
247
+ }
236
248
  case "response.abandoned": {
237
249
  if (parents.has(payload.response.responseId))
238
250
  interrupted.add(payload.response.responseId);
@@ -0,0 +1,2 @@
1
+ import type { NormalizedRealtimeFault, RealtimeFault } from "./types.js";
2
+ export declare function normalizeFault(fault: RealtimeFault): NormalizedRealtimeFault;
@@ -0,0 +1,25 @@
1
+ // Reuse SDK-normalized faults so events and rejected requests share one object.
2
+ const normalizedFaults = new WeakSet();
3
+ export function normalizeFault(fault) {
4
+ if (normalizedFaults.has(fault))
5
+ return fault;
6
+ const snapshot = {
7
+ code: fault.code,
8
+ message: fault.message,
9
+ scope: fault.scope,
10
+ requestId: fault.requestId,
11
+ responseId: fault.responseId,
12
+ itemId: fault.itemId,
13
+ callIds: Object.freeze([...(fault.callIds ?? [])]),
14
+ correlationId: fault.correlationId,
15
+ parameter: fault.parameter,
16
+ };
17
+ const normalized = Object.freeze({
18
+ ...snapshot,
19
+ retryable: snapshot.code === "capacity_exceeded" ||
20
+ snapshot.code === "unavailable" ||
21
+ snapshot.code === "response_timeout",
22
+ });
23
+ normalizedFaults.add(normalized);
24
+ return normalized;
25
+ }
@@ -18,9 +18,23 @@ export interface SessionSettings {
18
18
  }[];
19
19
  voice?: string;
20
20
  instructions?: string;
21
+ prism_response_metadata?: t.Labels;
22
+ }
23
+ /** A copy of the client's labels for every reply: keys to strings. */
24
+ export declare function responseMetadata(value: unknown): t.Labels;
25
+ export interface SessionPatch {
26
+ voice?: string;
27
+ instructions?: string;
28
+ tools?: SessionSettings["tools"];
29
+ turn_detection?: {
30
+ interrupt_response: boolean;
31
+ };
21
32
  }
22
33
  export interface SessionView {
34
+ settingsRevision: number | null;
23
35
  id: string;
36
+ requestId: string | null;
37
+ responseMetadata: t.Labels | null;
24
38
  voice: string;
25
39
  interruptOnSpeech: boolean | null;
26
40
  toolResultTimeoutS: number | null;
@@ -32,6 +46,7 @@ export interface SessionCreated {
32
46
  session: SessionView;
33
47
  }
34
48
  export interface SessionUpdated {
49
+ info?: t.SessionInfo;
35
50
  kind: "session.updated";
36
51
  session: SessionView;
37
52
  }
@@ -55,7 +70,7 @@ export interface ResponseEnded extends t.ResponseEnded {
55
70
  drainToken: string | null;
56
71
  }
57
72
  export type Acknowledgment = SessionUpdated | ItemCreated | ToolResultCreated | AudioCleared | ItemTruncated | t.ResponseStarted | ResponseEnded;
58
- export type ServerPayload = SessionCreated | Acknowledgment | t.InputEvent | t.MessageStarted | t.TextDelta | t.TextDone | t.AudioDelta | t.AudioDone | t.ToolCall | t.FaultEvent;
73
+ export type ServerPayload = SessionCreated | Acknowledgment | t.InputEvent<t.NormalizedRealtimeFault> | t.MessageStarted | t.TextDelta | t.TextDone | t.AudioDelta | t.AudioDone | t.ToolCall | t.FaultEvent<t.NormalizedRealtimeFault>;
59
74
  export interface ServerEvent {
60
75
  eventId: string;
61
76
  requestId: string | null;
@@ -1,6 +1,16 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  /** Decode only the protocol fields the SDK uses. Unknown events and fields stay open. */
3
- import { RimeStreamError } from "../errors.js";
3
+ import { RimeInputError, RimeStreamError } from "../errors.js";
4
+ import { normalizeFault } from "./faults.js";
5
+ /** A copy of the client's labels for every reply: keys to strings. */
6
+ export function responseMetadata(value) {
7
+ if (!value ||
8
+ typeof value !== "object" ||
9
+ ![Object.prototype, null].includes(Object.getPrototypeOf(value)) ||
10
+ Object.values(value).some((label) => typeof label !== "string"))
11
+ throw new RimeInputError("responseMetadata must map keys to strings");
12
+ return Object.freeze({ ...value });
13
+ }
4
14
  /** A request carries its result type and checks it before exposing the result. */
5
15
  export class Request {
6
16
  kind;
@@ -57,6 +67,11 @@ function integer(value, field) {
57
67
  return invalid(field);
58
68
  return value;
59
69
  }
70
+ function revision(value) {
71
+ return value.prism_settings_revision === undefined
72
+ ? null
73
+ : integer(value.prism_settings_revision, "prism_settings_revision");
74
+ }
60
75
  function duration(value, field) {
61
76
  if (value == null)
62
77
  return null;
@@ -100,7 +115,12 @@ function session(value) {
100
115
  if (interrupt !== undefined && typeof interrupt !== "boolean")
101
116
  invalid("session.prism_effective_interrupt_response");
102
117
  return {
118
+ settingsRevision: revision(view),
103
119
  id: id(view.id, "session.id"),
120
+ requestId: optionalString(view.request_id, "session.request_id"),
121
+ responseMetadata: view.prism_response_metadata === undefined
122
+ ? null
123
+ : clientMetadata(object(view.prism_response_metadata, "session.prism_response_metadata")),
104
124
  voice: view.voice === undefined ? "" : string(view.voice, "session.voice"),
105
125
  interruptOnSpeech: interrupt ?? null,
106
126
  turnDetection: Object.freeze({ eagerness, endOfTurnTimeoutMs }),
@@ -108,6 +128,11 @@ function session(value) {
108
128
  toolContinuationTimeoutS: duration(waits.continuation_timeout_s, "continuation_timeout_s"),
109
129
  };
110
130
  }
131
+ /** The client's labels in `metadata`: its string values under keys that are
132
+ * not Prism's `prism_` ones. */
133
+ function clientMetadata(metadata) {
134
+ return Object.freeze(Object.fromEntries(Object.entries(metadata).filter((entry) => !entry[0].startsWith("prism_") && typeof entry[1] === "string")));
135
+ }
111
136
  function fault(value) {
112
137
  const error = object(value, "error");
113
138
  const owner = error.owner == null ? {} : object(error.owner, "error.owner");
@@ -115,8 +140,9 @@ function fault(value) {
115
140
  const calls = owner.call_ids ?? [];
116
141
  if (!Array.isArray(calls))
117
142
  return invalid("error.owner.call_ids");
118
- return Object.freeze({
119
- code: error.code === undefined ? "unknown" : string(error.code, "error.code"),
143
+ const code = error.code === undefined ? "unknown" : string(error.code, "error.code");
144
+ return normalizeFault({
145
+ code,
120
146
  message: error.message === undefined ? "" : string(error.message, "error.message"),
121
147
  scope: scope === "event" ||
122
148
  scope === "utterance" ||
@@ -128,7 +154,7 @@ function fault(value) {
128
154
  requestId: optionalString(owner.event_id, "error.owner.event_id"),
129
155
  responseId: optionalString(owner.response_id, "error.owner.response_id"),
130
156
  itemId: optionalString(owner.item_id, "error.owner.item_id"),
131
- callIds: Object.freeze(calls.map((value) => id(value, "error.owner.call_ids"))),
157
+ callIds: calls.map((value) => id(value, "error.owner.call_ids")),
132
158
  correlationId: optionalString(error.correlation_id, "error.correlation_id"),
133
159
  parameter: optionalString(error.param, "error.param"),
134
160
  });
@@ -291,8 +317,14 @@ function decodeValue(value, sessionId) {
291
317
  ? undefined
292
318
  : string(metadata.prism_cause, "prism_cause");
293
319
  const parent = optionalString(metadata.prism_parent_response_id, "prism_parent_response_id");
320
+ const inputItemId = optionalString(metadata.prism_input_item_id, "prism_input_item_id");
321
+ const inputItemIds = response.prism_input_item_ids ??
322
+ (inputItemId === null ? [] : [inputItemId]);
323
+ if (!Array.isArray(inputItemIds))
324
+ return invalid("prism_input_item_ids");
294
325
  payload = {
295
326
  kind: "response.started",
327
+ settingsRevision: revision(response),
296
328
  response: ref(response.id),
297
329
  cause: cause === undefined
298
330
  ? "speech"
@@ -304,7 +336,9 @@ function decodeValue(value, sessionId) {
304
336
  ? "proactive"
305
337
  : "unknown",
306
338
  parent: parent === null ? null : ref(parent),
307
- inputItemId: optionalString(metadata.prism_input_item_id, "prism_input_item_id"),
339
+ inputItemId,
340
+ metadata: clientMetadata(metadata),
341
+ inputItemIds: Object.freeze(inputItemIds.map((value) => id(value, "prism_input_item_ids"))),
308
342
  };
309
343
  break;
310
344
  }
@@ -318,6 +352,7 @@ function decodeValue(value, sessionId) {
318
352
  const status = string(response.status, "response.status");
319
353
  payload = {
320
354
  kind: "response.ended",
355
+ settingsRevision: revision(response),
321
356
  response: ref(response.id),
322
357
  status: status === "completed" ||
323
358
  status === "cancelled" ||
@@ -1,6 +1,8 @@
1
1
  import { RealtimeConnection } from "./transport.js";
2
2
  import * as t from "./types.js";
3
3
  import * as p from "./protocol.js";
4
+ /** @internal Read the policy captured before queuing the response event. */
5
+ export declare function responseInterruptsSpeech(session: RealtimeSession, response: t.ResponseRef): boolean;
4
6
  /** @internal Recognize only failures created by intentional session closure. */
5
7
  export declare function isSessionCloseError(error: unknown): boolean;
6
8
  /** One conversation. Consume events concurrently with session operations. */
@@ -27,6 +29,7 @@ export declare class RealtimeSession {
27
29
  private readonly toolLock;
28
30
  private readonly pending;
29
31
  private readonly responses;
32
+ private readonly interruptionPolicies;
30
33
  private latest;
31
34
  private readonly tasks;
32
35
  private converter;
@@ -35,7 +38,7 @@ export declare class RealtimeSession {
35
38
  /** @internal */
36
39
  initialize(settings: p.SessionSettings, signal: AbortSignal): Promise<void>;
37
40
  get info(): t.SessionInfo;
38
- get events(): AsyncIterableIterator<t.SessionEvent>;
41
+ get events(): AsyncIterableIterator<t.SessionEvent<t.NormalizedRealtimeFault>>;
39
42
  private readEvents;
40
43
  private id;
41
44
  private check;
@@ -49,12 +52,19 @@ export declare class RealtimeSession {
49
52
  private runRequest;
50
53
  private abandon;
51
54
  private create;
55
+ /** Apply supplied settings and wait for acknowledgment. Tools replace the
56
+ * complete list. Replies already started keep their original settings.
57
+ * Cancellation after submission does not undo an accepted update. */
58
+ update(options?: t.RealtimeUpdateOptions): Promise<t.SessionInfo>;
52
59
  sendText(value: string, options?: t.RealtimeOperationOptions): Promise<t.ResponseRef>;
53
60
  requestReply(options?: t.ReplyOptions): Promise<t.ResponseRef>;
54
61
  addMessage(role: "user" | "assistant", value: string, options?: t.RealtimeOperationOptions): Promise<t.ItemRef>;
55
62
  submitToolResult(call: t.ToolCallRef, output: string, options?: t.RealtimeOperationOptions): Promise<void>;
56
63
  continueReply(parent: t.ResponseRef, options?: t.RealtimeOperationOptions): Promise<t.ResponseRef>;
57
64
  cancel(response: t.ResponseRef, options?: t.RealtimeOperationOptions): Promise<void>;
65
+ /** Replaces the labels later replies carry; a reply already started keeps
66
+ * its own. Resolves once Prism confirms the change. */
67
+ setResponseMetadata(metadata: t.Labels, options?: t.RealtimeOperationOptions): Promise<void>;
58
68
  clearAudio(options?: t.RealtimeOperationOptions): Promise<void>;
59
69
  sendAudio(chunk: t.AudioChunk, options?: t.RealtimeOperationOptions): Promise<void>;
60
70
  reportPlayback(report: t.PlaybackReport, options?: t.RealtimeOperationOptions): Promise<void>;
@@ -10,6 +10,7 @@ import { formatOf } from "./audio.js";
10
10
  import { InputConverter } from "../pcm.js";
11
11
  import * as t from "./types.js";
12
12
  import * as p from "./protocol.js";
13
+ import { InterruptionPolicies, toolSettings } from "./settings.js";
13
14
  const NOT_READY = new Set([
14
15
  "typed_turn_busy",
15
16
  "instructions_busy",
@@ -17,6 +18,11 @@ const NOT_READY = new Set([
17
18
  ]);
18
19
  const LIMIT = 128;
19
20
  const closeErrors = new WeakSet();
21
+ const responseInterruptions = new WeakMap();
22
+ /** @internal Read the policy captured before queuing the response event. */
23
+ export function responseInterruptsSpeech(session, response) {
24
+ return responseInterruptions.get(response) ?? session.info.interruptOnSpeech;
25
+ }
20
26
  /** @internal Recognize only failures created by intentional session closure. */
21
27
  export function isSessionCloseError(error) {
22
28
  return error instanceof Error && closeErrors.has(error);
@@ -70,6 +76,7 @@ export class RealtimeSession {
70
76
  toolLock = new Mutex();
71
77
  pending = new Map();
72
78
  responses = new Map();
79
+ interruptionPolicies = new InterruptionPolicies();
73
80
  latest = null;
74
81
  tasks = new Set();
75
82
  converter = null;
@@ -106,16 +113,7 @@ export class RealtimeSession {
106
113
  deadline.dispose();
107
114
  }
108
115
  const event = await this.request(p.requests.update, { session: settings }, signal);
109
- const view = event.session;
110
- this.sessionInfo = Object.freeze({
111
- sessionId: this.id(),
112
- model: "prism",
113
- voice: view.voice ?? "",
114
- interruptOnSpeech: view.interruptOnSpeech ?? settings.turn_detection.interrupt_response,
115
- toolResultTimeoutS: view.toolResultTimeoutS ?? null,
116
- toolContinuationTimeoutS: view.toolContinuationTimeoutS ?? null,
117
- turnDetection: view.turnDetection,
118
- });
116
+ this.sessionInfo = event.info;
119
117
  }
120
118
  get info() {
121
119
  if (!this.sessionInfo)
@@ -266,7 +264,14 @@ export class RealtimeSession {
266
264
  const kind = request.kind;
267
265
  const eventId = `evt_${randomUUID().replaceAll("-", "")}`;
268
266
  const result = new Deferred();
269
- this.pending.set(eventId, { request, result, target });
267
+ this.pending.set(eventId, {
268
+ request,
269
+ result,
270
+ target,
271
+ interruptOnSpeech: request.kind === "session.update"
272
+ ? body.session.turn_detection?.interrupt_response
273
+ : undefined,
274
+ });
270
275
  const deadline = scope([this.lifetime.signal], this.timeouts.requestS, new RimeTimeoutError(`${kind}: no acknowledgment; outcome unknown`, eventId));
271
276
  let submitted = false;
272
277
  // Cancellation can prevent a write. After submission, keep matching its outcome.
@@ -357,6 +362,33 @@ export class RealtimeSession {
357
362
  release();
358
363
  }
359
364
  }
365
+ /** Apply supplied settings and wait for acknowledgment. Tools replace the
366
+ * complete list. Replies already started keep their original settings.
367
+ * Cancellation after submission does not undo an accepted update. */
368
+ async update(options = {}) {
369
+ const settings = {};
370
+ for (const name of ["instructions", "voice"]) {
371
+ const value = options[name];
372
+ if (value !== undefined) {
373
+ if (typeof value !== "string")
374
+ throw new RimeInputError(`${name} must be a string`);
375
+ settings[name] = value;
376
+ }
377
+ }
378
+ if (options.tools !== undefined)
379
+ settings.tools = toolSettings(options.tools);
380
+ if (options.interruptOnSpeech !== undefined) {
381
+ if (typeof options.interruptOnSpeech !== "boolean")
382
+ throw new RimeInputError("interruptOnSpeech must be a boolean");
383
+ settings.turn_detection = {
384
+ interrupt_response: options.interruptOnSpeech,
385
+ };
386
+ }
387
+ return this.operation(options, async (signal) => {
388
+ const event = await this.request(p.requests.update, { session: settings }, signal);
389
+ return event.info;
390
+ });
391
+ }
360
392
  async sendText(value, options = {}) {
361
393
  text(value);
362
394
  return this.operation(options, (signal) => this.create({ prism_input_text: value, metadata: { prism_cause: "user_text" } }, signal, p.requests.text));
@@ -459,6 +491,14 @@ export class RealtimeSession {
459
491
  await this.request(p.requests.cancel, { response_id: state.ref.responseId }, signal, state.ref.responseId);
460
492
  }));
461
493
  }
494
+ /** Replaces the labels later replies carry; a reply already started keeps
495
+ * its own. Resolves once Prism confirms the change. */
496
+ async setResponseMetadata(metadata, options = {}) {
497
+ const labels = p.responseMetadata(metadata);
498
+ return this.operation(options, async (signal) => {
499
+ await this.request(p.requests.update, { session: { prism_response_metadata: labels } }, signal);
500
+ });
501
+ }
462
502
  async clearAudio(options = {}) {
463
503
  return this.operation(options, (signal) => this.locked(this.audioLock, signal, async () => {
464
504
  await this.request(p.requests.clear, {}, signal);
@@ -588,7 +628,30 @@ export class RealtimeSession {
588
628
  this.created.resolve(payload.session);
589
629
  }
590
630
  return;
591
- case "session.updated":
631
+ case "session.updated": {
632
+ const view = payload.session;
633
+ const pending = this.pending.get(echo ?? "");
634
+ if (!pending)
635
+ return;
636
+ this.sessionInfo = Object.freeze({
637
+ sessionId: this.id(),
638
+ model: "prism",
639
+ voice: view.voice,
640
+ interruptOnSpeech: view.interruptOnSpeech ??
641
+ pending?.interruptOnSpeech ??
642
+ this.sessionInfo?.interruptOnSpeech ??
643
+ true,
644
+ toolResultTimeoutS: view.toolResultTimeoutS,
645
+ toolContinuationTimeoutS: view.toolContinuationTimeoutS,
646
+ turnDetection: view.turnDetection,
647
+ settingsRevision: view.settingsRevision,
648
+ requestId: view.requestId,
649
+ responseMetadata: view.responseMetadata,
650
+ });
651
+ this.interruptionPolicies.record(view.settingsRevision, this.sessionInfo.interruptOnSpeech);
652
+ this.settle(echo, { ...payload, info: this.sessionInfo });
653
+ return;
654
+ }
592
655
  case "tool.result.created":
593
656
  this.settle(echo, payload);
594
657
  return;
@@ -630,6 +693,7 @@ export class RealtimeSession {
630
693
  const ref = payload.response;
631
694
  if (this.responses.has(ref.responseId))
632
695
  throw new RimeStreamError("Duplicate response.created");
696
+ responseInterruptions.set(ref, this.interruptionPolicies.resolve(payload.settingsRevision));
633
697
  this.responses.set(ref.responseId, new ResponseState(ref));
634
698
  for (const state of this.responses.values())
635
699
  state.changed.set();
@@ -0,0 +1,11 @@
1
+ import type { ToolDefinition } from "./types.js";
2
+ import type { SessionSettings } from "./protocol.js";
3
+ export declare function toolSettings(tools: readonly ToolDefinition[]): SessionSettings["tools"];
4
+ /** Bounded policy changes used to resolve each reply's settings revision. */
5
+ export declare class InterruptionPolicies {
6
+ private changes;
7
+ private current;
8
+ private lastRevision;
9
+ record(revision: number | null, interrupt: boolean): void;
10
+ resolve(revision: number | null | undefined): boolean;
11
+ }
@@ -0,0 +1,142 @@
1
+ import { RimeInputError, RimeResourceLimitError } from "../errors.js";
2
+ function toolString(value) {
3
+ // Unicode mode matches lone surrogates without rejecting valid UTF-16 pairs.
4
+ if (/[\uD800-\uDFFF]/u.test(value))
5
+ throw new RimeInputError("Tool strings must be valid UTF-8");
6
+ return value;
7
+ }
8
+ /** Reject values that JSON.stringify would silently drop or change. */
9
+ function copyJson(value) {
10
+ if (typeof value === "string")
11
+ return toolString(value);
12
+ if (value === null || typeof value === "boolean")
13
+ return value;
14
+ if (typeof value === "number" && Number.isFinite(value))
15
+ return value;
16
+ if (Array.isArray(value))
17
+ return Array.from(value, copyJson);
18
+ if (value &&
19
+ typeof value === "object" &&
20
+ [Object.prototype, null].includes(Object.getPrototypeOf(value)))
21
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
22
+ toolString(key),
23
+ copyJson(item),
24
+ ]));
25
+ throw new RimeInputError("Tool schemas must contain finite JSON values");
26
+ }
27
+ function fillerSettings(value) {
28
+ if (!value || typeof value !== "object" || Array.isArray(value))
29
+ throw new RimeInputError("prismFiller must be a filler object");
30
+ const kind = value.type;
31
+ if (!["none", "text", "auto"].includes(kind))
32
+ throw new RimeInputError("Prism filler type must be none, text, or auto");
33
+ const field = kind === "text" ? "text" : kind === "auto" ? "instructions" : null;
34
+ if (Object.entries(value).some(([key, item]) => item !== undefined && key !== "type" && key !== field))
35
+ throw new RimeInputError("Prism filler has fields not allowed for its type");
36
+ const text = field === null ? undefined : value[field];
37
+ if (kind === "text" || text !== undefined) {
38
+ // Rust str::trim uses Unicode White_Space, which differs from JS trim.
39
+ if (typeof text !== "string" ||
40
+ !/\P{White_Space}/u.test(text) ||
41
+ [...text].length > 500)
42
+ throw new RimeInputError("Prism filler text must be nonblank and at most 500 characters");
43
+ // Unicode mode matches lone surrogates but preserves valid UTF-16 pairs.
44
+ if (/[\uD800-\uDFFF]/u.test(text))
45
+ throw new RimeInputError("Prism filler text must be valid UTF-8");
46
+ }
47
+ if (kind === "text")
48
+ return { type: "text", text: text };
49
+ if (kind === "auto")
50
+ return text === undefined
51
+ ? { type: "auto" }
52
+ : { type: "auto", instructions: text };
53
+ return { type: "none" };
54
+ }
55
+ export function toolSettings(tools) {
56
+ if (!Array.isArray(tools))
57
+ throw new RimeInputError("tools must be an array");
58
+ const names = new Set();
59
+ return tools.map((tool) => {
60
+ if (!tool ||
61
+ typeof tool !== "object" ||
62
+ typeof tool.name !== "string" ||
63
+ !/\P{White_Space}/u.test(tool.name) ||
64
+ [...tool.name].length > 4000 ||
65
+ names.has(tool.name))
66
+ throw new RimeInputError("Tool names must be nonblank, unique, and at most 4000 characters");
67
+ toolString(tool.name);
68
+ names.add(tool.name);
69
+ if (!tool.parameters ||
70
+ Array.isArray(tool.parameters) ||
71
+ typeof tool.parameters !== "object" ||
72
+ (tool.description !== undefined && typeof tool.description !== "string"))
73
+ throw new RimeInputError("Tool parameters must be a JSON object and description must be a string");
74
+ let parameters;
75
+ try {
76
+ parameters = copyJson(tool.parameters);
77
+ }
78
+ catch (error) {
79
+ if (error instanceof RimeInputError)
80
+ throw error;
81
+ throw new RimeInputError("Tool schemas must contain finite JSON values");
82
+ }
83
+ if (parameters === null ||
84
+ typeof parameters !== "object" ||
85
+ Array.isArray(parameters))
86
+ throw new RimeInputError("Tool parameters must be an object");
87
+ return {
88
+ type: "function",
89
+ ...(tool.prismFiller === undefined
90
+ ? {}
91
+ : { prism_filler: fillerSettings(tool.prismFiller) }),
92
+ function: {
93
+ name: tool.name,
94
+ description: toolString(tool.description ?? ""),
95
+ parameters,
96
+ },
97
+ };
98
+ });
99
+ }
100
+ /** Bounded policy changes used to resolve each reply's settings revision. */
101
+ export class InterruptionPolicies {
102
+ changes = [];
103
+ current = true;
104
+ lastRevision = null;
105
+ record(revision, interrupt) {
106
+ const previous = this.changes.at(-1);
107
+ if (!previous) {
108
+ this.changes.push({ revision: revision ?? 0, interrupt });
109
+ }
110
+ else if (revision === null) {
111
+ if (interrupt !== this.current && !previous.unknown) {
112
+ // Earlier published revisions stay known; later ones are ambiguous
113
+ // until an acknowledgment supplies the new policy's revision.
114
+ this.changes.push({
115
+ revision: this.lastRevision === null ? 0 : this.lastRevision + 1,
116
+ interrupt,
117
+ unknown: true,
118
+ });
119
+ }
120
+ }
121
+ else if (previous.unknown || interrupt !== this.current) {
122
+ this.changes.push({ revision, interrupt });
123
+ }
124
+ if (revision !== null)
125
+ this.lastRevision = revision;
126
+ if (this.changes.length > 128)
127
+ this.changes.shift();
128
+ this.current = interrupt;
129
+ }
130
+ resolve(revision) {
131
+ if (revision == null || this.changes.length === 0)
132
+ return this.current;
133
+ for (let i = this.changes.length - 1; i >= 0; i--) {
134
+ if (this.changes[i].revision <= revision) {
135
+ if (this.changes[i].unknown)
136
+ throw new RimeResourceLimitError("Reply settings revision crosses an unreported interruption policy change");
137
+ return this.changes[i].interrupt;
138
+ }
139
+ }
140
+ throw new RimeResourceLimitError("Reply settings revision is no longer retained");
141
+ }
142
+ }
@@ -9,7 +9,7 @@ export declare class RealtimeConnection {
9
9
  readonly ready: Promise<void>;
10
10
  onMessage: (raw: string) => void;
11
11
  onFailure: (error: RimeError) => void;
12
- constructor(endpoint: string, authorization: string);
12
+ constructor(endpoint: string, authorization: string, requestId?: string);
13
13
  private failure;
14
14
  send(encoded: string): Promise<void>;
15
15
  close(): Promise<void>;
@@ -11,9 +11,12 @@ export class RealtimeConnection {
11
11
  ready = this.opened.promise;
12
12
  onMessage = () => { };
13
13
  onFailure = () => { };
14
- constructor(endpoint, authorization) {
14
+ constructor(endpoint, authorization, requestId) {
15
15
  this.socket = new WebSocket(endpoint, {
16
- headers: { Authorization: authorization },
16
+ headers: {
17
+ Authorization: authorization,
18
+ ...(requestId === undefined ? {} : { "x-request-id": requestId }),
19
+ },
17
20
  followRedirects: false,
18
21
  perMessageDeflate: false,
19
22
  maxPayload: 1024 * 1024,
@@ -2,6 +2,10 @@ import type { PCMFormat } from "../pcm.js";
2
2
  export type { PCMFormat } from "../pcm.js";
3
3
  import { RimeError, RimeTimeoutError } from "../errors.js";
4
4
  export type JsonValue = null | boolean | number | string | JsonValue[] | JsonObject;
5
+ /** Your labels for replies, keys to strings, such as `{ step: "3" }`. Prism
6
+ * takes at most 16, keys up to 64 characters and values up to 512, and
7
+ * refuses keys starting with `prism_`. */
8
+ export type Labels = Readonly<Record<string, string>>;
5
9
  export interface JsonObject {
6
10
  [key: string]: JsonValue;
7
11
  }
@@ -59,6 +63,22 @@ export interface RealtimeConnectOptions extends RealtimeOperationOptions {
59
63
  readonly interruptOnSpeech?: boolean;
60
64
  readonly turnDetection?: TurnDetection;
61
65
  readonly timeouts?: RealtimeTimeouts;
66
+ /** Sent as the upgrade's `x-request-id`. It must be nonblank printable ASCII, or
67
+ * `connect` throws `RimeInputError`. Prism uses it if it is at most 128
68
+ * characters, else makes its own; `info.requestId` reports the one it used.
69
+ * Omitted or `null` sends none. */
70
+ readonly requestId?: string | null;
71
+ /** Labels every reply's `response.started` carries in `metadata`, such as
72
+ * `{ step: "3" }`; change them with `session.setResponseMetadata`. Prism
73
+ * refuses keys starting with `prism_`. Omitted or `null` sets none. */
74
+ readonly responseMetadata?: Labels | null;
75
+ }
76
+ /** Omitted fields keep their values. Empty instructions or tools clear them. */
77
+ export interface RealtimeUpdateOptions extends RealtimeOperationOptions {
78
+ readonly instructions?: string;
79
+ readonly tools?: readonly ToolDefinition[];
80
+ readonly voice?: string;
81
+ readonly interruptOnSpeech?: boolean;
62
82
  }
63
83
  export interface ReplyOptions extends RealtimeOperationOptions {
64
84
  readonly instruction?: string | null;
@@ -86,7 +106,16 @@ export interface OutputRef {
86
106
  readonly contentIndex: number;
87
107
  }
88
108
  export interface SessionInfo {
109
+ /** Settings revision published by the server; null when unavailable.
110
+ * Optional on caller-created values for compatibility. */
111
+ readonly settingsRevision?: number | null;
89
112
  readonly sessionId: string;
113
+ /** The `x-request-id` Prism's logs name this session by. Null when the
114
+ * server does not publish it. */
115
+ readonly requestId: string | null;
116
+ /** The labels replies carry, as Prism last confirmed them. Null when the
117
+ * server does not publish them. */
118
+ readonly responseMetadata: Labels | null;
90
119
  readonly model: "prism";
91
120
  readonly voice: string;
92
121
  readonly interruptOnSpeech: boolean;
@@ -96,6 +125,9 @@ export interface SessionInfo {
96
125
  }
97
126
  export interface RealtimeFault {
98
127
  readonly code: string;
128
+ /** Optional for compatibility with caller-created faults. The SDK derives
129
+ * its output classification from the code and ignores supplied values. */
130
+ readonly retryable?: boolean;
99
131
  readonly message: string;
100
132
  readonly scope: "event" | "utterance" | "response" | "tool_roundtrip" | "session" | "unknown";
101
133
  readonly requestId: string | null;
@@ -105,10 +137,18 @@ export interface RealtimeFault {
105
137
  readonly correlationId: string | null;
106
138
  readonly parameter: string | null;
107
139
  }
140
+ /** An immutable fault produced by the SDK. */
141
+ export interface NormalizedRealtimeFault extends RealtimeFault {
142
+ /** Whether the same request, unchanged, may succeed on another attempt.
143
+ * True only for capacity_exceeded, unavailable, and response_timeout.
144
+ * Unknown codes return false. The application owns retry timing, limits,
145
+ * and recovery after partial output; this value does not replay a request. */
146
+ readonly retryable: boolean;
147
+ }
108
148
  export declare class RealtimeAdmissionTimeout extends RimeTimeoutError {
109
149
  }
110
150
  export declare class RimeRealtimeError extends RimeError {
111
- readonly fault: RealtimeFault;
151
+ readonly fault: NormalizedRealtimeFault;
112
152
  constructor(fault: RealtimeFault);
113
153
  }
114
154
  export interface InputReady {
@@ -159,17 +199,26 @@ export interface TranscriptFinal {
159
199
  readonly itemId: string;
160
200
  readonly text: string;
161
201
  }
162
- export interface TranscriptFailed {
202
+ export interface TranscriptFailed<Fault extends RealtimeFault = RealtimeFault> {
163
203
  readonly kind: "transcript.failed";
164
204
  readonly itemId: string;
165
- readonly error: RealtimeFault;
205
+ readonly error: Fault;
166
206
  }
167
207
  export interface ResponseStarted {
208
+ /** Settings revision published by the server; null when unavailable.
209
+ * Optional on caller-created values for compatibility. */
210
+ readonly settingsRevision?: number | null;
168
211
  readonly kind: "response.started";
169
212
  readonly response: ResponseRef;
170
213
  readonly cause: "speech" | "text" | "proactive" | "tools" | "unknown";
171
214
  readonly parent: ResponseRef | null;
172
215
  readonly inputItemId: string | null;
216
+ /** The session's labels when the reply began. Prism's `prism_` keys are
217
+ * left out. */
218
+ readonly metadata: Labels;
219
+ /** The user items the reply answers: a spoken reply's committed turns, or
220
+ * a typed turn's item. Empty when the server names none. */
221
+ readonly inputItemIds: readonly string[];
173
222
  }
174
223
  export interface MessageStarted {
175
224
  readonly kind: "message.started";
@@ -209,22 +258,25 @@ export interface ResponseAbandoned {
209
258
  readonly response: ResponseRef;
210
259
  }
211
260
  export interface ResponseEnded {
261
+ /** Settings revision published by the server; null when unavailable.
262
+ * Optional on caller-created values for compatibility. */
263
+ readonly settingsRevision?: number | null;
212
264
  readonly kind: "response.ended";
213
265
  readonly response: ResponseRef;
214
266
  readonly status: "completed" | "cancelled" | "failed" | "unknown";
215
267
  readonly reason: string | null;
216
268
  }
217
- export interface FaultEvent {
269
+ export interface FaultEvent<Fault extends RealtimeFault = RealtimeFault> {
218
270
  readonly kind: "error";
219
- readonly error: RealtimeFault;
271
+ readonly error: Fault;
220
272
  }
221
- export type InputEvent = InputReady | SpeechHint | SpeechStarted | SpeechStopped | InputCommitted | TranscriptDelta | TranscriptFinal | TranscriptFailed;
273
+ export type InputEvent<Fault extends RealtimeFault = RealtimeFault> = InputReady | SpeechHint | SpeechStarted | SpeechStopped | InputCommitted | TranscriptDelta | TranscriptFinal | TranscriptFailed<Fault>;
222
274
  export type ResponseEvent = ResponseStarted | ResponseAbandoned | MessageStarted | TextDelta | TextDone | AudioDelta | AudioDone | ToolCall | ResponseEnded;
223
- export interface SessionEvent {
275
+ export interface SessionEvent<Fault extends RealtimeFault = RealtimeFault> {
224
276
  readonly sessionId: string;
225
277
  readonly eventId: string;
226
278
  readonly requestId: string | null;
227
- readonly payload: InputEvent | ResponseEvent | FaultEvent;
279
+ readonly payload: InputEvent<Fault> | ResponseEvent | FaultEvent<Fault>;
228
280
  }
229
281
  export interface PlaybackInterrupted {
230
282
  readonly kind: "interrupted";
@@ -1,10 +1,12 @@
1
1
  import { RimeError, RimeTimeoutError } from "../errors.js";
2
+ import { normalizeFault } from "./faults.js";
2
3
  export class RealtimeAdmissionTimeout extends RimeTimeoutError {
3
4
  }
4
5
  export class RimeRealtimeError extends RimeError {
5
6
  fault;
6
7
  constructor(fault) {
7
- super(`${fault.code}: ${fault.message}`, fault.requestId);
8
- this.fault = fault;
8
+ const normalized = normalizeFault(fault);
9
+ super(`${normalized.code}: ${normalized.message}`, normalized.requestId);
10
+ this.fault = normalized;
9
11
  }
10
12
  }
package/docs/realtime.md CHANGED
@@ -110,10 +110,13 @@ The runner is the only event reader. Do not consume `session.events` at the same
110
110
  time, call `reportPlayback` independently, or send a second audio source. Other
111
111
  session methods remain available for manual tools and application actions.
112
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
113
+ On confirmed `speech.started`, the runner uses the interruption setting of the
114
+ latest reply that is still generating or has unmeasured physical playback. It
115
+ resolves that setting from the reply's revision when the session receives the
116
+ reply. Updates and late event consumption cannot change it. If interruption is
117
+ enabled, the runner stops affected output, waits for the interrupted receipt,
118
+ and reports the measured position. It ignores late audio for that response.
119
+ A `speech.hint` alone does not stop playback. A server
117
120
  `truncate_not_current` refusal is forwarded as an event; it does not stop a newer
118
121
  response. Finished reports wait for generation completion and every output
119
122
  receipt, including outstanding playback from earlier responses. Prism treats
@@ -152,8 +155,9 @@ tool execution outside the runner.
152
155
  ## Connect
153
156
 
154
157
  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.
158
+ initializes one conversation, and returns a `RealtimeSession`. Use
159
+ `session.update` to change instructions, tools, voice, or interruption.
160
+ Use `session.setResponseMetadata` to replace reply labels.
157
161
 
158
162
  | Option | Required? | Default and behavior |
159
163
  | --- | --- | --- |
@@ -165,15 +169,20 @@ once. Open a new session to change them.
165
169
  | `turnDetection` | No | Omitted settings use the server's turn timing defaults. |
166
170
  | `timeouts` | No | `{ connectS: 10, readyS: 30, requestS: 10 }`. Each field is optional. Units are seconds. |
167
171
  | `model` | No | `"prism"`, the only accepted value. |
172
+ | `requestId` | No | Your id for the session. Sent as `x-request-id`. It must be nonblank printable ASCII, or the SDK throws `RimeInputError`. Prism uses it if it is at most 128 characters, else makes its own. |
173
+ | `responseMetadata` | No | Labels every reply carries in `response.started.metadata`, such as `{ step: "3" }`: up to 16 strings, keys up to 64 characters and values up to 512, no key starting with `prism_`. Prism refuses labels that break these limits, or keys starting with `prism_`, with `RimeRealtimeError` code `invalid_session`. |
168
174
  | `signal` | No | An `AbortSignal` that cancels connection setup. It does not cancel an established session. |
169
175
 
170
176
  Credentials come from `new Rime({ apiKey: "..." })` or `RIME_API_KEY`. Prism uses
171
177
  `Authorization: Bearer <key>`, without a TTS token exchange. TTS endpoint, model,
172
178
  voice, and timeout settings do not apply to Realtime.
173
179
 
174
- After connection, `session.info` contains `sessionId`, `model`, `voice`,
175
- `interruptOnSpeech`, `turnDetection`, `toolResultTimeoutS`, and
176
- `toolContinuationTimeoutS`.
180
+ After connection, `session.info` contains `sessionId`, `requestId`, `model`,
181
+ `voice`, `interruptOnSpeech`, `turnDetection`, `responseMetadata`,
182
+ `toolResultTimeoutS`, and `toolContinuationTimeoutS`. `requestId` is the id
183
+ Prism's logs use; include it when you report a problem. It is `null` on servers
184
+ that do not send it. An event's or error's `requestId` names the SDK request
185
+ that caused it instead.
177
186
  Check the effective interruption setting there. Tool wait durations are total
178
187
  server deadlines, not remaining time. A `null` duration can mean an omitted
179
188
  field; it does not prove that no deadline exists.
@@ -215,12 +224,54 @@ server reported in its initialization acknowledgment. A missing field is
215
224
  default. An unrecognized server eagerness becomes `"unknown"`.
216
225
  `interruptOnSpeech` remains a separate connection option.
217
226
 
227
+ ## Update session settings
228
+
229
+ This method is available in the repository source and requires an SDK release
230
+ that includes session updates.
231
+
232
+ ```ts
233
+ const info = await session.update({
234
+ instructions: "Handle billing. Use only the supplied tools.",
235
+ tools: [refundTool], // An existing ToolDefinition.
236
+ });
237
+ console.log(info.settingsRevision);
238
+
239
+ // For a transfer triggered by a tool call:
240
+ await session.submitToolResult(call, "Transferred to billing.");
241
+ await session.continueReply(parent);
242
+ ```
243
+
244
+ `RealtimeUpdateOptions` has optional `instructions`, `tools`, `voice`,
245
+ `interruptOnSpeech`, and `signal` fields. Omitted fields and `undefined` keep
246
+ their values. `null` is not accepted.
247
+
248
+ Omitted fields keep their values. A supplied tool list replaces the complete
249
+ list. Empty instructions clear the prompt; an empty tool list removes all tools.
250
+ An empty voice selects the deployment default. Instructions and tools supplied
251
+ together change in one request.
252
+
253
+ The method waits for the matching `session.updated` acknowledgment and returns
254
+ session information. The stored session information changes only after that
255
+ acknowledgment. A rejected update leaves it unchanged and keeps the connection
256
+ open. Cancellation after submission does not undo an accepted update.
257
+
258
+ Updates do not wait for turn readiness or tool results. Each reply keeps the
259
+ settings it began with. A reply prepared on an early turn-end guess can be
260
+ announced after an update but still use the previous settings revision.
261
+ Modalities, eagerness, and the end-of-turn timeout cannot change mid-session.
262
+
263
+ `SessionInfo`, `ResponseStarted`, and `ResponseEnded` expose `settingsRevision`.
264
+ It is `null` when the server does not publish it. The field is optional on
265
+ caller-created values. Users do not need to manage it. Use each reply's revision
266
+ to identify its settings, rather than the latest session revision.
267
+
218
268
  ## Choose an operation
219
269
 
220
270
  Every method returns a promise. Arguments without `?` are required.
221
271
 
222
272
  | Method | Result and behavior |
223
273
  | --- | --- |
274
+ | `update(options?)` | Changes supplied settings without reconnecting; returns `SessionInfo` after acknowledgment. |
224
275
  | `sendText(text, options?)` | Starts a typed user turn after readiness; returns `ResponseRef` at response creation. |
225
276
  | `sendAudio(chunk, options?)` | Sends microphone audio with transport backpressure. |
226
277
  | `requestReply(options?)` | Requests a reply without new user input; returns `ResponseRef`. Options can include `instruction` and `toolCall`. |
@@ -229,12 +280,13 @@ Every method returns a promise. Arguments without `?` are required.
229
280
  | `continueReply(parent, options?)` | Waits for parent completion and all tool result acknowledgments, then returns a new `ResponseRef`. |
230
281
  | `cancel(response, options?)` | Requests generation cancellation and waits for a terminal response or refusal. |
231
282
  | `clearAudio(options?)` | Clears buffered input and resets audio conversion after acknowledgment. |
283
+ | `setResponseMetadata(metadata, options?)` | Replaces the labels for later replies once Prism confirms. |
232
284
  | `reportPlayback(report, options?)` | Reports interrupted or finished playback. |
233
285
  | `close()` | Closes the session and releases its resources. |
234
286
 
235
287
  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>`.
288
+ instruction, tool call, and signal in one object. Methods other than `update`,
289
+ `sendText`, `requestReply`, `addMessage`, and `continueReply` return `Promise<void>`.
238
290
 
239
291
  Text must be nonblank and contain at most 4,000 Unicode characters. History
240
292
  roles are `"user"` and `"assistant"`. A reply instruction applies to one response;
@@ -304,7 +356,7 @@ also checks TypeScript callers.
304
356
 
305
357
  Filler applies when the reply has said nothing before calling a tool. If a reply
306
358
  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.
359
+ filler only; it does not silence other speech. Use `session.update({ tools })` to replace tools and their filler settings.
308
360
 
309
361
  Execute a tool once for each `tool.call` event. Validate its parsed `arguments`,
310
362
  then submit a string result with `submitToolResult(payload.call, output)`.
@@ -337,7 +389,7 @@ are exported from `@rimelabs/sdk`.
337
389
  | `input.committed` | `itemId` |
338
390
  | `transcript.delta`, `transcript.final` | `itemId` and `delta` or `text` |
339
391
  | `transcript.failed` | `itemId`, `error` |
340
- | `response.started` | `response`, `cause`, nullable `parent` and `inputItemId` |
392
+ | `response.started` | `response`, `cause`, nullable `parent` and `inputItemId`, `inputItemIds` (the user items it answers), `metadata` (your own keys) |
341
393
  | `message.started` | `output` |
342
394
  | `text.delta`, `text.done` | `output` and `delta` or `text` |
343
395
  | `audio.delta`, `audio.done` | `output`; a delta also has `audio.data` and a complete `audio.format` |
@@ -406,6 +458,53 @@ with `RimeRealtimeError`. Its `.fault` has `code`, `message`, `scope`, nullable
406
458
  array. A response fault does not itself end generation. Session faults and lost
407
459
  connections end the session.
408
460
 
461
+ `NormalizedRealtimeFault.retryable` reports whether the same request, unchanged,
462
+ may succeed on another attempt. Read `event.payload.error.retryable` on an `error`
463
+ event or `error.fault.retryable` on `RimeRealtimeError`. The SDK derives this
464
+ value from the code; Prism does not send a retry flag.
465
+
466
+ `RealtimeFault` accepts the previous fault shape, with an optional `retryable`
467
+ field, for caller-created faults. `RimeRealtimeError` accepts this shape and
468
+ ignores any supplied retry flag. Its `.fault` and SDK-emitted event faults are
469
+ `NormalizedRealtimeFault` values with a required boolean `retryable` field.
470
+ The SDK freezes these faults and their `callIds` arrays. Later changes to a
471
+ constructor input do not change the error's fault.
472
+
473
+ `TranscriptFailed`, `FaultEvent`, `InputEvent`, and `SessionEvent` default to
474
+ `RealtimeFault`, so existing event literals can omit `retryable`. These types
475
+ also accept a fault type parameter. `session.events` and the `runConversation`
476
+ event callback use `SessionEvent<NormalizedRealtimeFault>`, which gives their
477
+ faults a required boolean `retryable` field. An explicit `SessionEvent`
478
+ annotation uses the default type and makes `retryable` optional.
479
+
480
+ | Error code | `retryable` |
481
+ | --- | --- |
482
+ | `capacity_exceeded` | `true` |
483
+ | `unavailable` | `true` |
484
+ | `response_timeout` | `true` |
485
+ | `context_length_exceeded` | `false` |
486
+ | All other codes, including unknown codes | `false` |
487
+
488
+ The application chooses the delay, attempt limit, and recovery operation. This
489
+ value does not start a retry. It does not describe a local `RimeTimeoutError` or
490
+ prove that no output or side effect occurred. The SDK's existing readiness
491
+ retries have separate rules: an explicit refusal such as `typed_turn_busy`
492
+ permits another attempt after a new readiness event, although its `retryable`
493
+ value is `false` under this contract.
494
+
495
+ For example, Prism can accept a user's question and then report
496
+ `capacity_exceeded`. `sendText()` has already returned a `ResponseRef`; the
497
+ failure arrives through an `error` event. Wait for `response.ended`, then apply
498
+ the application's retry policy. If the question is already in history, use
499
+ `requestReply()` to ask for another reply when appropriate. Repeating
500
+ `sendText()` can add the question twice. A `response_timeout` can follow partial
501
+ text or audio, which the application must handle before another attempt.
502
+
503
+ For `context_length_exceeded`, the server message contains token counts.
504
+ Unchanged attempts will fail until the conversation is shorter. Start a new
505
+ session with shorter context. The protocol also supports history deletion, but
506
+ this SDK does not yet expose `conversation.item.delete`.
507
+
409
508
  Malformed fields in known events and mismatched request acknowledgments close
410
509
  the session with `RimeStreamError`. Unknown event kinds and extra fields remain
411
510
  accepted and ignored. The SDK does not replay a request after a protocol failure.
@@ -420,7 +519,19 @@ unknown outcome.
420
519
 
421
520
  Keep up with the event stream. The queue holds 256 events; incoming messages
422
521
  are limited to 1 MiB. A slow consumer ends the session. There is no automatic
423
- reconnect, history replay, or live configuration update.
522
+ reconnect or history replay.
424
523
 
425
524
  Local peer tests cover the SDK protocol handling. Validate the gateway handshake
426
525
  and endpoint behavior for the deployment you use.
526
+
527
+ The SDK retains the latest 128 interruption policy changes. Instructions or tools
528
+ updates that keep the same interruption policy do not consume this history.
529
+ If an unannounced reply refers to an older evicted policy, the session fails
530
+ with a resource limit error instead of guessing its policy. A reply already
531
+ received keeps its captured policy even after later settings updates.
532
+
533
+ If an interruption setting changes without a published revision, later reply
534
+ revisions can be ambiguous. The SDK preserves earlier known settings and raises
535
+ a resource limit error for an ambiguous reply instead of guessing. A later
536
+ acknowledgment with a revision establishes a new known setting. Replies without
537
+ a revision use the current acknowledged setting.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rimelabs/sdk",
3
- "version": "0.1.0-alpha.20",
3
+ "version": "0.1.0-alpha.21",
4
4
  "description": "Rime streaming TTS, STT and Prism realtime SDK for Node.js",
5
5
  "license": "MIT",
6
6
  "type": "module",