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

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,36 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.0-alpha.22](https://github.com/rimelabs/rime-sdk/compare/typescript-v0.1.0-alpha.21...typescript-v0.1.0-alpha.22) (2026-10-24)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * Rust SessionInfo struct literals must add truncation: None. The optional field reports the policy published by the server.
9
+ * Go Prism service faults now match ErrRealtime instead of ErrStream. Rust service faults now use ErrorKind::Realtime instead of Stream. Keep fault inspection for code, scope, and retry classification.
10
+
11
+ ### Features
12
+
13
+ * add Prism conversation history operations ([#108](https://github.com/rimelabs/rime-sdk/issues/108)) ([1f33051](https://github.com/rimelabs/rime-sdk/commit/1f330517163cd521bd03321a0728295f84da0316))
14
+ * add Prism history truncation controls ([#109](https://github.com/rimelabs/rime-sdk/issues/109)) ([b8b1aa1](https://github.com/rimelabs/rime-sdk/commit/b8b1aa172e69df650bfeda274bb91329f7e5bc52))
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * distinguish Prism faults and correct SDK release guidance ([#102](https://github.com/rimelabs/rime-sdk/issues/102)) ([b152abb](https://github.com/rimelabs/rime-sdk/commit/b152abb1ac4e5dabb92e747c04a6bc70aa72b60e))
20
+
21
+ ## [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)
22
+
23
+
24
+ ### ⚠ BREAKING CHANGES
25
+
26
+ * Rust RealtimeSession::info returns an owned SessionInfo snapshot instead of a reference so cloned handles can read current settings.
27
+
28
+ ### Features
29
+
30
+ * expose Prism error retry classification ([#99](https://github.com/rimelabs/rime-sdk/issues/99)) ([c04309c](https://github.com/rimelabs/rime-sdk/commit/c04309c174c2eed82df3b676581c45cab2a95db6))
31
+ * **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))
32
+ * 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))
33
+
3
34
  ## [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
35
 
5
36
 
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, truncationSettings } 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,47 +54,14 @@ 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
  };
63
+ if (options.truncation !== undefined)
64
+ settings.truncation = truncationSettings(options.truncation);
138
65
  if (options.turnDetection !== undefined) {
139
66
  const detection = options.turnDetection;
140
67
  if (!detection ||
@@ -153,6 +80,14 @@ export class Realtime {
153
80
  settings.turn_detection.prism_end_of_turn_timeout_ms = value;
154
81
  }
155
82
  }
83
+ if (options.responseMetadata != null)
84
+ settings.prism_response_metadata = responseMetadata(options.responseMetadata);
85
+ const requestId = options.requestId ?? undefined;
86
+ if (requestId !== undefined &&
87
+ (typeof requestId !== "string" ||
88
+ !requestId.trim() ||
89
+ !/^[\x20-\x7e]+$/.test(requestId)))
90
+ throw new RimeInputError("requestId must be nonblank printable ASCII");
156
91
  for (const name of ["voice", "instructions"]) {
157
92
  const value = options[name];
158
93
  if (value !== undefined && value !== null) {
@@ -170,7 +105,7 @@ export class Realtime {
170
105
  openingScope.signal.throwIfAborted();
171
106
  let socket;
172
107
  try {
173
- socket = new RealtimeConnection(address, this.credentials.realtimeAuthorization());
108
+ socket = new RealtimeConnection(address, this.credentials.realtimeAuthorization(), requestId);
174
109
  }
175
110
  catch {
176
111
  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
+ }
@@ -0,0 +1,4 @@
1
+ import type { HistoryItem } from "./types.js";
2
+ export declare function itemSettings(item: HistoryItem): Record<string, string>;
3
+ export declare function historyId(value: string): string;
4
+ export declare function listedItem(value: unknown): HistoryItem;
@@ -0,0 +1,87 @@
1
+ import { RimeInputError, RimeStreamError } from "../errors.js";
2
+ function utf8(value) {
3
+ return typeof value === "string" && !/[\uD800-\uDFFF]/u.test(value);
4
+ }
5
+ export function itemSettings(item) {
6
+ if (!item || typeof item !== "object")
7
+ throw new RimeInputError("history item must be an object");
8
+ const value = { type: item.type };
9
+ if (item.id !== undefined) {
10
+ if (!utf8(item.id) || [...item.id].length < 1 || [...item.id].length > 32)
11
+ throw new RimeInputError("item.id must contain 1 to 32 characters");
12
+ value.id = item.id;
13
+ }
14
+ switch (item.type) {
15
+ case "message":
16
+ if (!["user", "assistant", "system"].includes(item.role) ||
17
+ !utf8(item.text) ||
18
+ [...item.text].length > 4000)
19
+ throw new RimeInputError("invalid history message");
20
+ value.role = item.role;
21
+ value.text = item.text;
22
+ break;
23
+ case "function_call":
24
+ value.call_id = item.callId;
25
+ value.name = item.name;
26
+ value.arguments = item.arguments;
27
+ if (!item.callId || !item.name)
28
+ throw new RimeInputError("past calls require callId and name");
29
+ break;
30
+ case "function_call_output":
31
+ value.call_id = item.callId;
32
+ value.output = item.output;
33
+ if (!item.callId)
34
+ throw new RimeInputError("tool results require callId");
35
+ break;
36
+ default:
37
+ throw new RimeInputError("invalid history item type");
38
+ }
39
+ if (!Object.values(value).every(utf8))
40
+ throw new RimeInputError("history fields must be valid UTF-8 strings");
41
+ return value;
42
+ }
43
+ export function historyId(value) {
44
+ if (!utf8(value) || !value)
45
+ throw new RimeInputError("history ID must be nonempty valid UTF-8");
46
+ return value;
47
+ }
48
+ export function listedItem(value) {
49
+ if (!value || typeof value !== "object" || Array.isArray(value))
50
+ throw new RimeStreamError("invalid history item");
51
+ const o = value;
52
+ const field = (name) => {
53
+ if (typeof o[name] !== "string")
54
+ throw new RimeStreamError("invalid history field " + name);
55
+ if ((name === "call_id" || name === "name") && !o[name])
56
+ throw new RimeStreamError("invalid history identity");
57
+ return o[name];
58
+ };
59
+ const id = field("id");
60
+ if (!id)
61
+ throw new RimeStreamError("invalid history item id");
62
+ switch (o.type) {
63
+ case "message": {
64
+ const role = field("role");
65
+ if (role !== "user" && role !== "assistant" && role !== "system")
66
+ throw new RimeStreamError("invalid history role");
67
+ return Object.freeze({ type: o.type, id, role, text: field("text") });
68
+ }
69
+ case "function_call":
70
+ return Object.freeze({
71
+ type: o.type,
72
+ id,
73
+ callId: field("call_id"),
74
+ name: field("name"),
75
+ arguments: field("arguments"),
76
+ });
77
+ case "function_call_output":
78
+ return Object.freeze({
79
+ type: o.type,
80
+ id,
81
+ callId: field("call_id"),
82
+ output: field("output"),
83
+ });
84
+ default:
85
+ throw new RimeStreamError("invalid history item type");
86
+ }
87
+ }
@@ -1,5 +1,9 @@
1
1
  import * as t from "./types.js";
2
2
  export interface SessionSettings {
3
+ truncation?: "auto" | "disabled" | {
4
+ type: "retention_ratio";
5
+ retention_ratio: number;
6
+ };
3
7
  modalities: ("text" | "audio")[];
4
8
  input_audio_format: "pcm16";
5
9
  turn_detection: {
@@ -18,9 +22,25 @@ export interface SessionSettings {
18
22
  }[];
19
23
  voice?: string;
20
24
  instructions?: string;
25
+ prism_response_metadata?: t.Labels;
26
+ }
27
+ /** A copy of the client's labels for every reply: keys to strings. */
28
+ export declare function responseMetadata(value: unknown): t.Labels;
29
+ export interface SessionPatch {
30
+ truncation?: SessionSettings["truncation"];
31
+ voice?: string;
32
+ instructions?: string;
33
+ tools?: SessionSettings["tools"];
34
+ turn_detection?: {
35
+ interrupt_response: boolean;
36
+ };
21
37
  }
22
38
  export interface SessionView {
39
+ truncation: t.Truncation | null;
40
+ settingsRevision: number | null;
23
41
  id: string;
42
+ requestId: string | null;
43
+ responseMetadata: t.Labels | null;
24
44
  voice: string;
25
45
  interruptOnSpeech: boolean | null;
26
46
  toolResultTimeoutS: number | null;
@@ -32,6 +52,7 @@ export interface SessionCreated {
32
52
  session: SessionView;
33
53
  }
34
54
  export interface SessionUpdated {
55
+ info?: t.SessionInfo;
35
56
  kind: "session.updated";
36
57
  session: SessionView;
37
58
  }
@@ -40,9 +61,18 @@ export interface ItemCreated {
40
61
  itemId: string;
41
62
  }
42
63
  export interface ToolResultCreated {
64
+ itemId: string | null;
43
65
  kind: "tool.result.created";
44
66
  callId: string;
45
67
  }
68
+ export interface HistoryListed {
69
+ kind: "history.listed";
70
+ items: readonly t.HistoryItem[];
71
+ }
72
+ export interface ItemDeleted {
73
+ kind: "item.deleted";
74
+ itemId: string;
75
+ }
46
76
  export interface AudioCleared {
47
77
  kind: "audio.cleared";
48
78
  }
@@ -54,14 +84,14 @@ export interface ItemTruncated {
54
84
  export interface ResponseEnded extends t.ResponseEnded {
55
85
  drainToken: string | null;
56
86
  }
57
- 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;
87
+ export type Acknowledgment = SessionUpdated | HistoryListed | ItemDeleted | ItemCreated | ToolResultCreated | AudioCleared | ItemTruncated | t.ResponseStarted | ResponseEnded;
88
+ 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
89
  export interface ServerEvent {
60
90
  eventId: string;
61
91
  requestId: string | null;
62
92
  payload: ServerPayload;
63
93
  }
64
- export type RequestKind = "session.update" | "conversation.item.create" | "response.create" | "response.cancel" | "input_audio_buffer.clear" | "conversation.item.truncate";
94
+ export type RequestKind = "session.update" | "conversation.item.create" | "prism.conversation.get" | "conversation.item.delete" | "response.create" | "response.cancel" | "input_audio_buffer.clear" | "conversation.item.truncate";
65
95
  /** A request carries its result type and checks it before exposing the result. */
66
96
  export declare class Request<T extends Acknowledgment> {
67
97
  readonly kind: RequestKind;
@@ -72,6 +102,10 @@ export declare class Request<T extends Acknowledgment> {
72
102
  check(event: Acknowledgment): void;
73
103
  }
74
104
  export declare const requests: {
105
+ historyItem: Request<ItemCreated>;
106
+ historyResult: Request<ToolResultCreated>;
107
+ history: Request<HistoryListed>;
108
+ delete: Request<ItemDeleted>;
75
109
  update: Request<SessionUpdated>;
76
110
  item: Request<ItemCreated>;
77
111
  toolResult: Request<ToolResultCreated>;
@@ -81,4 +115,5 @@ export declare const requests: {
81
115
  clear: Request<AudioCleared>;
82
116
  truncate: Request<ItemTruncated>;
83
117
  };
118
+ export declare function truncationSettings(value: t.Truncation): NonNullable<SessionSettings["truncation"]>;
84
119
  export declare function decode(raw: string, sessionId: string | null): ServerEvent | null;
@@ -1,6 +1,17 @@
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
+ import { listedItem } from "./history.js";
6
+ /** A copy of the client's labels for every reply: keys to strings. */
7
+ export function responseMetadata(value) {
8
+ if (!value ||
9
+ typeof value !== "object" ||
10
+ ![Object.prototype, null].includes(Object.getPrototypeOf(value)) ||
11
+ Object.values(value).some((label) => typeof label !== "string"))
12
+ throw new RimeInputError("responseMetadata must map keys to strings");
13
+ return Object.freeze({ ...value });
14
+ }
4
15
  /** A request carries its result type and checks it before exposing the result. */
5
16
  export class Request {
6
17
  kind;
@@ -21,6 +32,10 @@ export class Request {
21
32
  }
22
33
  }
23
34
  export const requests = {
35
+ historyItem: new Request("conversation.item.create", (e) => e.kind === "item.created"),
36
+ historyResult: new Request("conversation.item.create", (e) => e.kind === "tool.result.created"),
37
+ history: new Request("prism.conversation.get", (e) => e.kind === "history.listed"),
38
+ delete: new Request("conversation.item.delete", (e) => e.kind === "item.deleted"),
24
39
  update: new Request("session.update", (e) => e.kind === "session.updated"),
25
40
  item: new Request("conversation.item.create", (e) => e.kind === "item.created"),
26
41
  toolResult: new Request("conversation.item.create", (e) => e.kind === "tool.result.created"),
@@ -57,6 +72,11 @@ function integer(value, field) {
57
72
  return invalid(field);
58
73
  return value;
59
74
  }
75
+ function revision(value) {
76
+ return value.prism_settings_revision === undefined
77
+ ? null
78
+ : integer(value.prism_settings_revision, "prism_settings_revision");
79
+ }
60
80
  function duration(value, field) {
61
81
  if (value == null)
62
82
  return null;
@@ -64,6 +84,29 @@ function duration(value, field) {
64
84
  return invalid(field);
65
85
  return value;
66
86
  }
87
+ export function truncationSettings(value) {
88
+ if (value === "auto" || value === "disabled")
89
+ return value;
90
+ if (value &&
91
+ typeof value === "object" &&
92
+ !Array.isArray(value) &&
93
+ Object.keys(value).every((key) => key === "retentionRatio") &&
94
+ typeof value.retentionRatio === "number" &&
95
+ value.retentionRatio > 0 &&
96
+ value.retentionRatio <= 1)
97
+ return { type: "retention_ratio", retention_ratio: value.retentionRatio };
98
+ throw new RimeInputError("truncation must be auto, disabled, or a retentionRatio above zero and at most one");
99
+ }
100
+ function truncation(value) {
101
+ if (value === "auto" || value === "disabled")
102
+ return value;
103
+ const o = object(value, "session.truncation");
104
+ if (o.type !== "retention_ratio" ||
105
+ typeof o.retention_ratio !== "number" ||
106
+ !(o.retention_ratio > 0 && o.retention_ratio <= 1))
107
+ return invalid("session.truncation");
108
+ return Object.freeze({ retentionRatio: o.retention_ratio });
109
+ }
67
110
  function session(value) {
68
111
  const view = object(value, "session");
69
112
  const detection = view.turn_detection === undefined
@@ -100,7 +143,13 @@ function session(value) {
100
143
  if (interrupt !== undefined && typeof interrupt !== "boolean")
101
144
  invalid("session.prism_effective_interrupt_response");
102
145
  return {
146
+ settingsRevision: revision(view),
147
+ truncation: view.truncation === undefined ? null : truncation(view.truncation),
103
148
  id: id(view.id, "session.id"),
149
+ requestId: optionalString(view.request_id, "session.request_id"),
150
+ responseMetadata: view.prism_response_metadata === undefined
151
+ ? null
152
+ : clientMetadata(object(view.prism_response_metadata, "session.prism_response_metadata")),
104
153
  voice: view.voice === undefined ? "" : string(view.voice, "session.voice"),
105
154
  interruptOnSpeech: interrupt ?? null,
106
155
  turnDetection: Object.freeze({ eagerness, endOfTurnTimeoutMs }),
@@ -108,6 +157,11 @@ function session(value) {
108
157
  toolContinuationTimeoutS: duration(waits.continuation_timeout_s, "continuation_timeout_s"),
109
158
  };
110
159
  }
160
+ /** The client's labels in `metadata`: its string values under keys that are
161
+ * not Prism's `prism_` ones. */
162
+ function clientMetadata(metadata) {
163
+ return Object.freeze(Object.fromEntries(Object.entries(metadata).filter((entry) => !entry[0].startsWith("prism_") && typeof entry[1] === "string")));
164
+ }
111
165
  function fault(value) {
112
166
  const error = object(value, "error");
113
167
  const owner = error.owner == null ? {} : object(error.owner, "error.owner");
@@ -115,8 +169,9 @@ function fault(value) {
115
169
  const calls = owner.call_ids ?? [];
116
170
  if (!Array.isArray(calls))
117
171
  return invalid("error.owner.call_ids");
118
- return Object.freeze({
119
- code: error.code === undefined ? "unknown" : string(error.code, "error.code"),
172
+ const code = error.code === undefined ? "unknown" : string(error.code, "error.code");
173
+ return normalizeFault({
174
+ code,
120
175
  message: error.message === undefined ? "" : string(error.message, "error.message"),
121
176
  scope: scope === "event" ||
122
177
  scope === "utterance" ||
@@ -128,7 +183,7 @@ function fault(value) {
128
183
  requestId: optionalString(owner.event_id, "error.owner.event_id"),
129
184
  responseId: optionalString(owner.response_id, "error.owner.response_id"),
130
185
  itemId: optionalString(owner.item_id, "error.owner.item_id"),
131
- callIds: Object.freeze(calls.map((value) => id(value, "error.owner.call_ids"))),
186
+ callIds: calls.map((value) => id(value, "error.owner.call_ids")),
132
187
  correlationId: optionalString(error.correlation_id, "error.correlation_id"),
133
188
  parameter: optionalString(error.param, "error.param"),
134
189
  });
@@ -195,15 +250,29 @@ function decodeValue(value, sessionId) {
195
250
  if (item.type === "function_call_output")
196
251
  payload = {
197
252
  kind: "tool.result.created",
253
+ itemId: optionalString(item.id, "item.id"),
198
254
  callId: id(item.call_id, "item.call_id"),
199
255
  };
200
256
  else {
201
- if (item.type !== undefined && item.type !== "message")
257
+ if (item.type !== undefined &&
258
+ item.type !== "message" &&
259
+ item.type !== "function_call")
202
260
  invalid("item.type");
203
261
  payload = { kind: "item.created", itemId: id(item.id, "item.id") };
204
262
  }
205
263
  break;
206
264
  }
265
+ case "prism.conversation":
266
+ if (!Array.isArray(event.items))
267
+ return invalid("items");
268
+ payload = {
269
+ kind: "history.listed",
270
+ items: Object.freeze(event.items.map(listedItem)),
271
+ };
272
+ break;
273
+ case "conversation.item.deleted":
274
+ payload = { kind: "item.deleted", itemId: id(event.item_id, "item_id") };
275
+ break;
207
276
  case "input_audio_buffer.cleared":
208
277
  payload = { kind: "audio.cleared" };
209
278
  break;
@@ -291,8 +360,14 @@ function decodeValue(value, sessionId) {
291
360
  ? undefined
292
361
  : string(metadata.prism_cause, "prism_cause");
293
362
  const parent = optionalString(metadata.prism_parent_response_id, "prism_parent_response_id");
363
+ const inputItemId = optionalString(metadata.prism_input_item_id, "prism_input_item_id");
364
+ const inputItemIds = response.prism_input_item_ids ??
365
+ (inputItemId === null ? [] : [inputItemId]);
366
+ if (!Array.isArray(inputItemIds))
367
+ return invalid("prism_input_item_ids");
294
368
  payload = {
295
369
  kind: "response.started",
370
+ settingsRevision: revision(response),
296
371
  response: ref(response.id),
297
372
  cause: cause === undefined
298
373
  ? "speech"
@@ -304,7 +379,9 @@ function decodeValue(value, sessionId) {
304
379
  ? "proactive"
305
380
  : "unknown",
306
381
  parent: parent === null ? null : ref(parent),
307
- inputItemId: optionalString(metadata.prism_input_item_id, "prism_input_item_id"),
382
+ inputItemId,
383
+ metadata: clientMetadata(metadata),
384
+ inputItemIds: Object.freeze(inputItemIds.map((value) => id(value, "prism_input_item_ids"))),
308
385
  };
309
386
  break;
310
387
  }
@@ -318,6 +395,7 @@ function decodeValue(value, sessionId) {
318
395
  const status = string(response.status, "response.status");
319
396
  payload = {
320
397
  kind: "response.ended",
398
+ settingsRevision: revision(response),
321
399
  response: ref(response.id),
322
400
  status: status === "completed" ||
323
401
  status === "cancelled" ||