@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
|
@@ -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,25 @@ 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>;
|
|
61
|
+
/** Read the items the next reply would see, in server order. */
|
|
62
|
+
getHistory(options?: t.RealtimeOperationOptions): Promise<readonly t.HistoryItem[]>;
|
|
63
|
+
/** Hide an item from future replies. A call and its result are hidden together. */
|
|
64
|
+
deleteItem(itemId: string, options?: t.RealtimeOperationOptions): Promise<void>;
|
|
65
|
+
/** Add history without a reply. The server owns placement and identity checks. */
|
|
66
|
+
createItem(item: t.HistoryItem, options?: t.HistoryCreateOptions): Promise<t.ItemRef>;
|
|
54
67
|
addMessage(role: "user" | "assistant", value: string, options?: t.RealtimeOperationOptions): Promise<t.ItemRef>;
|
|
55
68
|
submitToolResult(call: t.ToolCallRef, output: string, options?: t.RealtimeOperationOptions): Promise<void>;
|
|
56
69
|
continueReply(parent: t.ResponseRef, options?: t.RealtimeOperationOptions): Promise<t.ResponseRef>;
|
|
57
70
|
cancel(response: t.ResponseRef, options?: t.RealtimeOperationOptions): Promise<void>;
|
|
71
|
+
/** Replaces the labels later replies carry; a reply already started keeps
|
|
72
|
+
* its own. Resolves once Prism confirms the change. */
|
|
73
|
+
setResponseMetadata(metadata: t.Labels, options?: t.RealtimeOperationOptions): Promise<void>;
|
|
58
74
|
clearAudio(options?: t.RealtimeOperationOptions): Promise<void>;
|
|
59
75
|
sendAudio(chunk: t.AudioChunk, options?: t.RealtimeOperationOptions): Promise<void>;
|
|
60
76
|
reportPlayback(report: t.PlaybackReport, options?: t.RealtimeOperationOptions): Promise<void>;
|
package/dist/realtime/session.js
CHANGED
|
@@ -10,6 +10,8 @@ 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 { historyId, itemSettings } from "./history.js";
|
|
14
|
+
import { InterruptionPolicies, toolSettings } from "./settings.js";
|
|
13
15
|
const NOT_READY = new Set([
|
|
14
16
|
"typed_turn_busy",
|
|
15
17
|
"instructions_busy",
|
|
@@ -17,6 +19,11 @@ const NOT_READY = new Set([
|
|
|
17
19
|
]);
|
|
18
20
|
const LIMIT = 128;
|
|
19
21
|
const closeErrors = new WeakSet();
|
|
22
|
+
const responseInterruptions = new WeakMap();
|
|
23
|
+
/** @internal Read the policy captured before queuing the response event. */
|
|
24
|
+
export function responseInterruptsSpeech(session, response) {
|
|
25
|
+
return responseInterruptions.get(response) ?? session.info.interruptOnSpeech;
|
|
26
|
+
}
|
|
20
27
|
/** @internal Recognize only failures created by intentional session closure. */
|
|
21
28
|
export function isSessionCloseError(error) {
|
|
22
29
|
return error instanceof Error && closeErrors.has(error);
|
|
@@ -70,6 +77,7 @@ export class RealtimeSession {
|
|
|
70
77
|
toolLock = new Mutex();
|
|
71
78
|
pending = new Map();
|
|
72
79
|
responses = new Map();
|
|
80
|
+
interruptionPolicies = new InterruptionPolicies();
|
|
73
81
|
latest = null;
|
|
74
82
|
tasks = new Set();
|
|
75
83
|
converter = null;
|
|
@@ -106,16 +114,7 @@ export class RealtimeSession {
|
|
|
106
114
|
deadline.dispose();
|
|
107
115
|
}
|
|
108
116
|
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
|
-
});
|
|
117
|
+
this.sessionInfo = event.info;
|
|
119
118
|
}
|
|
120
119
|
get info() {
|
|
121
120
|
if (!this.sessionInfo)
|
|
@@ -266,7 +265,23 @@ export class RealtimeSession {
|
|
|
266
265
|
const kind = request.kind;
|
|
267
266
|
const eventId = `evt_${randomUUID().replaceAll("-", "")}`;
|
|
268
267
|
const result = new Deferred();
|
|
269
|
-
this.pending.set(eventId, {
|
|
268
|
+
this.pending.set(eventId, {
|
|
269
|
+
request,
|
|
270
|
+
result,
|
|
271
|
+
target,
|
|
272
|
+
requiresItemId: request === p.requests.historyItem ||
|
|
273
|
+
request === p.requests.historyResult,
|
|
274
|
+
expectedItemId: body.item?.id,
|
|
275
|
+
toolOutputDigest: request.kind === "conversation.item.create" &&
|
|
276
|
+
body.item?.type === "function_call_output"
|
|
277
|
+
? createHash("sha256")
|
|
278
|
+
.update(body.item.output)
|
|
279
|
+
.digest("hex")
|
|
280
|
+
: undefined,
|
|
281
|
+
interruptOnSpeech: request.kind === "session.update"
|
|
282
|
+
? body.session.turn_detection?.interrupt_response
|
|
283
|
+
: undefined,
|
|
284
|
+
});
|
|
270
285
|
const deadline = scope([this.lifetime.signal], this.timeouts.requestS, new RimeTimeoutError(`${kind}: no acknowledgment; outcome unknown`, eventId));
|
|
271
286
|
let submitted = false;
|
|
272
287
|
// Cancellation can prevent a write. After submission, keep matching its outcome.
|
|
@@ -357,6 +372,35 @@ export class RealtimeSession {
|
|
|
357
372
|
release();
|
|
358
373
|
}
|
|
359
374
|
}
|
|
375
|
+
/** Apply supplied settings and wait for acknowledgment. Tools replace the
|
|
376
|
+
* complete list. Replies already started keep their original settings.
|
|
377
|
+
* Cancellation after submission does not undo an accepted update. */
|
|
378
|
+
async update(options = {}) {
|
|
379
|
+
const settings = {};
|
|
380
|
+
if (options.truncation !== undefined)
|
|
381
|
+
settings.truncation = p.truncationSettings(options.truncation);
|
|
382
|
+
for (const name of ["instructions", "voice"]) {
|
|
383
|
+
const value = options[name];
|
|
384
|
+
if (value !== undefined) {
|
|
385
|
+
if (typeof value !== "string")
|
|
386
|
+
throw new RimeInputError(`${name} must be a string`);
|
|
387
|
+
settings[name] = value;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (options.tools !== undefined)
|
|
391
|
+
settings.tools = toolSettings(options.tools);
|
|
392
|
+
if (options.interruptOnSpeech !== undefined) {
|
|
393
|
+
if (typeof options.interruptOnSpeech !== "boolean")
|
|
394
|
+
throw new RimeInputError("interruptOnSpeech must be a boolean");
|
|
395
|
+
settings.turn_detection = {
|
|
396
|
+
interrupt_response: options.interruptOnSpeech,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
return this.operation(options, async (signal) => {
|
|
400
|
+
const event = await this.request(p.requests.update, { session: settings }, signal);
|
|
401
|
+
return event.info;
|
|
402
|
+
});
|
|
403
|
+
}
|
|
360
404
|
async sendText(value, options = {}) {
|
|
361
405
|
text(value);
|
|
362
406
|
return this.operation(options, (signal) => this.create({ prism_input_text: value, metadata: { prism_cause: "user_text" } }, signal, p.requests.text));
|
|
@@ -377,18 +421,52 @@ export class RealtimeSession {
|
|
|
377
421
|
}
|
|
378
422
|
return this.operation(options, (signal) => this.create(body, signal));
|
|
379
423
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
424
|
+
/** Read the items the next reply would see, in server order. */
|
|
425
|
+
async getHistory(options = {}) {
|
|
426
|
+
return this.operation(options, async (signal) => (await this.request(p.requests.history, {}, signal)).items);
|
|
427
|
+
}
|
|
428
|
+
/** Hide an item from future replies. A call and its result are hidden together. */
|
|
429
|
+
async deleteItem(itemId, options = {}) {
|
|
430
|
+
if (typeof itemId !== "string" || !itemId)
|
|
431
|
+
throw new RimeInputError("itemId must be nonempty");
|
|
432
|
+
historyId(itemId);
|
|
384
433
|
return this.operation(options, async (signal) => {
|
|
385
|
-
|
|
434
|
+
await this.request(p.requests.delete, { item_id: itemId }, signal, itemId);
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
/** Add history without a reply. The server owns placement and identity checks. */
|
|
438
|
+
async createItem(item, options = {}) {
|
|
439
|
+
const value = itemSettings(item);
|
|
440
|
+
const body = { item: value };
|
|
441
|
+
if (options.previousItemId !== undefined) {
|
|
442
|
+
if (typeof options.previousItemId !== "string" ||
|
|
443
|
+
!options.previousItemId ||
|
|
444
|
+
item.type === "function_call_output")
|
|
445
|
+
throw new RimeInputError("invalid previousItemId for history item");
|
|
446
|
+
body.previous_item_id = historyId(options.previousItemId);
|
|
447
|
+
}
|
|
448
|
+
return this.operation(options, async (signal) => {
|
|
449
|
+
const event = value.type === "function_call_output"
|
|
450
|
+
? await this.request(p.requests.historyResult, body, signal, value.call_id)
|
|
451
|
+
: await this.request(p.requests.historyItem, body, signal);
|
|
452
|
+
if (!event.itemId ||
|
|
453
|
+
(value.id !== undefined && event.itemId !== value.id)) {
|
|
454
|
+
const error = new RimeStreamError("history acknowledgment names an invalid item");
|
|
455
|
+
this.fail(error);
|
|
456
|
+
throw error;
|
|
457
|
+
}
|
|
386
458
|
return Object.freeze({
|
|
387
459
|
sessionId: this.info.sessionId,
|
|
388
460
|
itemId: event.itemId,
|
|
389
461
|
});
|
|
390
462
|
});
|
|
391
463
|
}
|
|
464
|
+
async addMessage(role, value, options = {}) {
|
|
465
|
+
if (role !== "user" && role !== "assistant")
|
|
466
|
+
throw new RimeInputError("History role must be user or assistant");
|
|
467
|
+
text(value);
|
|
468
|
+
return this.createItem({ type: "message", role, text: value }, { signal: options.signal });
|
|
469
|
+
}
|
|
392
470
|
async submitToolResult(call, output, options = {}) {
|
|
393
471
|
const ref = { ...call };
|
|
394
472
|
this.response(ref);
|
|
@@ -459,6 +537,14 @@ export class RealtimeSession {
|
|
|
459
537
|
await this.request(p.requests.cancel, { response_id: state.ref.responseId }, signal, state.ref.responseId);
|
|
460
538
|
}));
|
|
461
539
|
}
|
|
540
|
+
/** Replaces the labels later replies carry; a reply already started keeps
|
|
541
|
+
* its own. Resolves once Prism confirms the change. */
|
|
542
|
+
async setResponseMetadata(metadata, options = {}) {
|
|
543
|
+
const labels = p.responseMetadata(metadata);
|
|
544
|
+
return this.operation(options, async (signal) => {
|
|
545
|
+
await this.request(p.requests.update, { session: { prism_response_metadata: labels } }, signal);
|
|
546
|
+
});
|
|
547
|
+
}
|
|
462
548
|
async clearAudio(options = {}) {
|
|
463
549
|
return this.operation(options, (signal) => this.locked(this.audioLock, signal, async () => {
|
|
464
550
|
await this.request(p.requests.clear, {}, signal);
|
|
@@ -569,6 +655,8 @@ export class RealtimeSession {
|
|
|
569
655
|
const { payload, requestId: echo } = event;
|
|
570
656
|
// Check echoed acknowledgments before changing response or item state.
|
|
571
657
|
if (payload.kind === "session.updated" ||
|
|
658
|
+
payload.kind === "history.listed" ||
|
|
659
|
+
payload.kind === "item.deleted" ||
|
|
572
660
|
payload.kind === "item.created" ||
|
|
573
661
|
payload.kind === "tool.result.created" ||
|
|
574
662
|
payload.kind === "response.started") {
|
|
@@ -576,6 +664,13 @@ export class RealtimeSession {
|
|
|
576
664
|
if (pending &&
|
|
577
665
|
!(payload.kind === "item.created" && pending.request.allowsItemCreated))
|
|
578
666
|
pending.request.check(payload);
|
|
667
|
+
if (pending?.requiresItemId &&
|
|
668
|
+
((payload.kind !== "item.created" &&
|
|
669
|
+
payload.kind !== "tool.result.created") ||
|
|
670
|
+
!payload.itemId ||
|
|
671
|
+
(pending.expectedItemId !== undefined &&
|
|
672
|
+
pending.expectedItemId !== payload.itemId)))
|
|
673
|
+
throw new RimeStreamError("history acknowledgment names an invalid item");
|
|
579
674
|
if (pending &&
|
|
580
675
|
payload.kind === "tool.result.created" &&
|
|
581
676
|
pending.target !== payload.callId)
|
|
@@ -588,10 +683,53 @@ export class RealtimeSession {
|
|
|
588
683
|
this.created.resolve(payload.session);
|
|
589
684
|
}
|
|
590
685
|
return;
|
|
591
|
-
case "session.updated":
|
|
592
|
-
|
|
686
|
+
case "session.updated": {
|
|
687
|
+
const view = payload.session;
|
|
688
|
+
const pending = this.pending.get(echo ?? "");
|
|
689
|
+
if (!pending)
|
|
690
|
+
return;
|
|
691
|
+
this.sessionInfo = Object.freeze({
|
|
692
|
+
sessionId: this.id(),
|
|
693
|
+
model: "prism",
|
|
694
|
+
voice: view.voice,
|
|
695
|
+
interruptOnSpeech: view.interruptOnSpeech ??
|
|
696
|
+
pending?.interruptOnSpeech ??
|
|
697
|
+
this.sessionInfo?.interruptOnSpeech ??
|
|
698
|
+
true,
|
|
699
|
+
toolResultTimeoutS: view.toolResultTimeoutS,
|
|
700
|
+
toolContinuationTimeoutS: view.toolContinuationTimeoutS,
|
|
701
|
+
turnDetection: view.turnDetection,
|
|
702
|
+
settingsRevision: view.settingsRevision,
|
|
703
|
+
truncation: view.truncation,
|
|
704
|
+
requestId: view.requestId,
|
|
705
|
+
responseMetadata: view.responseMetadata,
|
|
706
|
+
});
|
|
707
|
+
this.interruptionPolicies.record(view.settingsRevision, this.sessionInfo.interruptOnSpeech);
|
|
708
|
+
this.settle(echo, { ...payload, info: this.sessionInfo });
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
case "history.listed":
|
|
712
|
+
this.settle(echo, payload);
|
|
713
|
+
return;
|
|
714
|
+
case "item.deleted": {
|
|
715
|
+
const pending = this.pending.get(echo ?? "");
|
|
716
|
+
if (pending && pending.target !== payload.itemId)
|
|
717
|
+
throw new RimeStreamError("deletion acknowledgment names another item");
|
|
593
718
|
this.settle(echo, payload);
|
|
594
719
|
return;
|
|
720
|
+
}
|
|
721
|
+
case "tool.result.created": {
|
|
722
|
+
const pending = this.pending.get(echo ?? "");
|
|
723
|
+
if (pending?.toolOutputDigest !== undefined)
|
|
724
|
+
for (const response of this.responses.values()) {
|
|
725
|
+
if (response.calls.get(payload.callId) === null) {
|
|
726
|
+
response.calls.set(payload.callId, pending.toolOutputDigest);
|
|
727
|
+
response.changed.set();
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
this.settle(echo, payload);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
595
733
|
case "item.created": {
|
|
596
734
|
const pending = this.pending.get(echo ?? "");
|
|
597
735
|
if (pending?.request.allowsItemCreated)
|
|
@@ -630,6 +768,7 @@ export class RealtimeSession {
|
|
|
630
768
|
const ref = payload.response;
|
|
631
769
|
if (this.responses.has(ref.responseId))
|
|
632
770
|
throw new RimeStreamError("Duplicate response.created");
|
|
771
|
+
responseInterruptions.set(ref, this.interruptionPolicies.resolve(payload.settingsRevision));
|
|
633
772
|
this.responses.set(ref.responseId, new ResponseState(ref));
|
|
634
773
|
for (const state of this.responses.values())
|
|
635
774
|
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,
|