@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 +31 -0
- package/README.md +1 -0
- package/dist/realtime/client.js +14 -79
- package/dist/realtime/conversation.d.ts +2 -2
- package/dist/realtime/conversation.js +16 -4
- package/dist/realtime/faults.d.ts +2 -0
- package/dist/realtime/faults.js +25 -0
- package/dist/realtime/history.d.ts +4 -0
- package/dist/realtime/history.js +87 -0
- package/dist/realtime/protocol.d.ts +38 -3
- package/dist/realtime/protocol.js +84 -6
- package/dist/realtime/session.d.ts +17 -1
- package/dist/realtime/session.js +157 -18
- package/dist/realtime/settings.d.ts +11 -0
- package/dist/realtime/settings.js +142 -0
- package/dist/realtime/transport.d.ts +1 -1
- package/dist/realtime/transport.js +5 -2
- package/dist/realtime/types.d.ts +91 -8
- package/dist/realtime/types.js +4 -2
- package/docs/realtime.md +181 -18
- package/package.json +1 -1
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).
|
package/dist/realtime/client.js
CHANGED
|
@@ -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 ?? [])
|
|
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
|
-
|
|
234
|
-
|
|
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,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,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
|
-
|
|
119
|
-
|
|
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:
|
|
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 &&
|
|
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
|
|
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" ||
|