@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 +13 -0
- package/README.md +1 -0
- package/dist/realtime/client.js +12 -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/protocol.d.ts +16 -1
- package/dist/realtime/protocol.js +40 -5
- package/dist/realtime/session.d.ts +11 -1
- package/dist/realtime/session.js +76 -12
- 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 +60 -8
- package/dist/realtime/types.js +4 -2
- package/docs/realtime.md +125 -14
- package/package.json +1 -1
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).
|
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 } 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 ?? [])
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -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
|
-
|
|
119
|
-
|
|
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:
|
|
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
|
|
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>;
|
package/dist/realtime/session.js
CHANGED
|
@@ -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
|
-
|
|
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, {
|
|
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: {
|
|
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,
|
package/dist/realtime/types.d.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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";
|
package/dist/realtime/types.js
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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`.
|
|
156
|
-
|
|
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`, `
|
|
175
|
-
`interruptOnSpeech`, `turnDetection`, `
|
|
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 `
|
|
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.
|
|
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
|
|
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.
|