dsh-realtime 0.1.0 → 0.2.0
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/lib/error.d.ts +48 -2
- package/lib/error.d.ts.map +1 -0
- package/lib/error.js +56 -1
- package/lib/error.js.map +1 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js.map +1 -0
- package/lib/types.d.ts +15 -3
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js.map +1 -0
- package/package.json +4 -7
- package/src/error.ts +144 -0
- package/src/index.ts +320 -0
- package/src/types.ts +208 -0
package/lib/error.d.ts
CHANGED
|
@@ -26,9 +26,51 @@ export declare const REALTIME_ERROR_CODES: Readonly<{
|
|
|
26
26
|
PROVIDER_ERROR: "PROVIDER_ERROR";
|
|
27
27
|
/** A recorded session could not be read as a recording. */
|
|
28
28
|
INVALID_RECORDING: "INVALID_RECORDING";
|
|
29
|
+
/**
|
|
30
|
+
* The capability is not configured yet — an expected state on a fresh install, not a fault.
|
|
31
|
+
*
|
|
32
|
+
* Distinct from {@link REALTIME_ERROR_CODES.CREDENTIAL_REJECTED}: absent is fixed by supplying a
|
|
33
|
+
* value, unusable by replacing one.
|
|
34
|
+
*/
|
|
35
|
+
NOT_CONFIGURED: "NOT_CONFIGURED";
|
|
36
|
+
/** A credential is present and the provider refused it. Replace it; retrying cannot help. */
|
|
37
|
+
CREDENTIAL_REJECTED: "CREDENTIAL_REJECTED";
|
|
38
|
+
/** The credential is valid but the account cannot use what was requested (model, region, scope). */
|
|
39
|
+
NOT_ENTITLED: "NOT_ENTITLED";
|
|
40
|
+
/** The account cannot pay for the request — an exhausted balance or a hard billing limit. */
|
|
41
|
+
INSUFFICIENT_CREDIT: "INSUFFICIENT_CREDIT";
|
|
42
|
+
/** The provider throttled the request. Repeating later may succeed. */
|
|
43
|
+
RATE_LIMITED: "RATE_LIMITED";
|
|
44
|
+
/** The provider was asked something and did not answer within the bound. */
|
|
45
|
+
PROVIDER_TIMEOUT: "PROVIDER_TIMEOUT";
|
|
46
|
+
/** The transport failed — a socket error, not a decision the provider made. */
|
|
47
|
+
NETWORK: "NETWORK";
|
|
29
48
|
}>;
|
|
30
49
|
/** One of {@link REALTIME_ERROR_CODES}. */
|
|
31
50
|
export type RealtimeErrorCode = (typeof REALTIME_ERROR_CODES)[keyof typeof REALTIME_ERROR_CODES];
|
|
51
|
+
/**
|
|
52
|
+
* What a caller can act on, carried as structure rather than prose.
|
|
53
|
+
*
|
|
54
|
+
* A failure a user cannot act on is a failure report that wasted their time. Every field here exists
|
|
55
|
+
* so the *class* of a failure survives translation: an agent can relay `remedy` verbatim, a settings
|
|
56
|
+
* surface can highlight `setting`, a retry loop can consult `retryable`, and a human reading a log
|
|
57
|
+
* can see the provider's own `providerCode` instead of a paraphrase of it.
|
|
58
|
+
*
|
|
59
|
+
* Never carries secret material. `providerCode` is the provider's own error code, never a credential.
|
|
60
|
+
*/
|
|
61
|
+
export interface RealtimeFailureDetail {
|
|
62
|
+
/**
|
|
63
|
+
* What to do about it, written to be relayed **verbatim** to whoever is trying to use the feature.
|
|
64
|
+
* One sentence, imperative, no jargon.
|
|
65
|
+
*/
|
|
66
|
+
remedy?: string;
|
|
67
|
+
/** The configuration key that must be supplied, when the failure is a configuration state. */
|
|
68
|
+
setting?: string;
|
|
69
|
+
/** Whether repeating the identical call could succeed without any change. */
|
|
70
|
+
retryable?: boolean;
|
|
71
|
+
/** The provider's own error code, verbatim. Never a translated or inferred value. */
|
|
72
|
+
providerCode?: string;
|
|
73
|
+
}
|
|
32
74
|
/**
|
|
33
75
|
* A typed seam failure.
|
|
34
76
|
*
|
|
@@ -38,11 +80,15 @@ export type RealtimeErrorCode = (typeof REALTIME_ERROR_CODES)[keyof typeof REALT
|
|
|
38
80
|
export declare class RealtimeError extends Error {
|
|
39
81
|
/** Stable machine code. Branch on this, never on `message`. */
|
|
40
82
|
readonly code: RealtimeErrorCode;
|
|
83
|
+
/** What a caller can act on, when the failure is one they can act on. */
|
|
84
|
+
readonly detail: RealtimeFailureDetail | undefined;
|
|
41
85
|
/**
|
|
42
86
|
* @param message - non-empty human-readable summary. Must not contain secret material.
|
|
43
87
|
* @param code - one of {@link REALTIME_ERROR_CODES}.
|
|
44
|
-
* @param options - optional `cause`.
|
|
88
|
+
* @param options - optional `cause`, and an optional structured `detail`.
|
|
45
89
|
*/
|
|
46
|
-
constructor(message: string, code: RealtimeErrorCode, options?: ErrorOptions
|
|
90
|
+
constructor(message: string, code: RealtimeErrorCode, options?: ErrorOptions & {
|
|
91
|
+
detail?: RealtimeFailureDetail;
|
|
92
|
+
});
|
|
47
93
|
}
|
|
48
94
|
//# sourceMappingURL=error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,gDAAgD;AAChD,eAAO,MAAM,oBAAoB;IAC/B,iEAAiE;;IAEjE,yEAAyE;;IAEzE,yEAAyE;;IAEzE,wDAAwD;;IAExD,mEAAmE;;IAEnE,2CAA2C;;IAE3C,iGAAiG;;IAEjG,iGAAiG;;IAEjG,2DAA2D;;IAS3D;;;;;OAKG;;IAEH,6FAA6F;;IAE7F,oGAAoG;;IAEpG,6FAA6F;;IAE7F,uEAAuE;;IAEvE,4EAA4E;;IAE5E,+EAA+E;;EAE/E,CAAA;AAEF,2CAA2C;AAC3C,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,OAAO,oBAAoB,CAAC,CAAA;AAEhG;;;;;;;;;GASG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,8FAA8F;IAC9F,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,qFAAqF;IACrF,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AA4BD;;;;;GAKG;AACH,qBAAa,aAAc,SAAQ,KAAK;IACtC,+DAA+D;IAC/D,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAA;IAEhC,yEAAyE;IACzE,QAAQ,CAAC,MAAM,EAAE,qBAAqB,GAAG,SAAS,CAAA;IAElD;;;;OAIG;IACH,YAAY,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG;QAAE,MAAM,CAAC,EAAE,qBAAqB,CAAA;KAAE,EAchH;CACF"}
|
package/lib/error.js
CHANGED
|
@@ -26,7 +26,56 @@ export const REALTIME_ERROR_CODES = Object.freeze({
|
|
|
26
26
|
PROVIDER_ERROR: 'PROVIDER_ERROR',
|
|
27
27
|
/** A recorded session could not be read as a recording. */
|
|
28
28
|
INVALID_RECORDING: 'INVALID_RECORDING',
|
|
29
|
+
// ---------------------------------------------------------------------------------------------
|
|
30
|
+
// Failure taxonomy. These exist so a consumer can branch on the *class* of a failure rather than
|
|
31
|
+
// parse prose: a setup state, a rejected credential, an account that cannot pay, and a throttle
|
|
32
|
+
// need four different responses, and collapsing them is what makes onboarding feel unfinished.
|
|
33
|
+
// ---------------------------------------------------------------------------------------------
|
|
34
|
+
/**
|
|
35
|
+
* The capability is not configured yet — an expected state on a fresh install, not a fault.
|
|
36
|
+
*
|
|
37
|
+
* Distinct from {@link REALTIME_ERROR_CODES.CREDENTIAL_REJECTED}: absent is fixed by supplying a
|
|
38
|
+
* value, unusable by replacing one.
|
|
39
|
+
*/
|
|
40
|
+
NOT_CONFIGURED: 'NOT_CONFIGURED',
|
|
41
|
+
/** A credential is present and the provider refused it. Replace it; retrying cannot help. */
|
|
42
|
+
CREDENTIAL_REJECTED: 'CREDENTIAL_REJECTED',
|
|
43
|
+
/** The credential is valid but the account cannot use what was requested (model, region, scope). */
|
|
44
|
+
NOT_ENTITLED: 'NOT_ENTITLED',
|
|
45
|
+
/** The account cannot pay for the request — an exhausted balance or a hard billing limit. */
|
|
46
|
+
INSUFFICIENT_CREDIT: 'INSUFFICIENT_CREDIT',
|
|
47
|
+
/** The provider throttled the request. Repeating later may succeed. */
|
|
48
|
+
RATE_LIMITED: 'RATE_LIMITED',
|
|
49
|
+
/** The provider was asked something and did not answer within the bound. */
|
|
50
|
+
PROVIDER_TIMEOUT: 'PROVIDER_TIMEOUT',
|
|
51
|
+
/** The transport failed — a socket error, not a decision the provider made. */
|
|
52
|
+
NETWORK: 'NETWORK',
|
|
53
|
+
});
|
|
54
|
+
/** Expected `typeof` for each optional detail field. Data, so the check costs one loop and not a branch each. */
|
|
55
|
+
const DETAIL_FIELD_TYPES = Object.freeze({
|
|
56
|
+
remedy: 'string',
|
|
57
|
+
setting: 'string',
|
|
58
|
+
providerCode: 'string',
|
|
59
|
+
retryable: 'boolean',
|
|
29
60
|
});
|
|
61
|
+
/**
|
|
62
|
+
* Reject a malformed detail before it is attached to a failure.
|
|
63
|
+
* @param detail - candidate detail.
|
|
64
|
+
* @throws TypeError naming the offending field.
|
|
65
|
+
*/
|
|
66
|
+
function assertDetail(detail) {
|
|
67
|
+
for (const [field, expected] of Object.entries(DETAIL_FIELD_TYPES)) {
|
|
68
|
+
const value = detail[field];
|
|
69
|
+
if (value === undefined)
|
|
70
|
+
continue;
|
|
71
|
+
if (typeof value !== expected) {
|
|
72
|
+
throw new TypeError(`RealtimeError detail.${field} must be a ${expected}`);
|
|
73
|
+
}
|
|
74
|
+
if (expected === 'string' && value === '') {
|
|
75
|
+
throw new TypeError(`RealtimeError detail.${field} must be non-empty when supplied`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
30
79
|
/**
|
|
31
80
|
* A typed seam failure.
|
|
32
81
|
*
|
|
@@ -36,10 +85,12 @@ export const REALTIME_ERROR_CODES = Object.freeze({
|
|
|
36
85
|
export class RealtimeError extends Error {
|
|
37
86
|
/** Stable machine code. Branch on this, never on `message`. */
|
|
38
87
|
code;
|
|
88
|
+
/** What a caller can act on, when the failure is one they can act on. */
|
|
89
|
+
detail;
|
|
39
90
|
/**
|
|
40
91
|
* @param message - non-empty human-readable summary. Must not contain secret material.
|
|
41
92
|
* @param code - one of {@link REALTIME_ERROR_CODES}.
|
|
42
|
-
* @param options - optional `cause`.
|
|
93
|
+
* @param options - optional `cause`, and an optional structured `detail`.
|
|
43
94
|
*/
|
|
44
95
|
constructor(message, code, options) {
|
|
45
96
|
if (typeof message !== 'string' || message.length === 0) {
|
|
@@ -48,9 +99,13 @@ export class RealtimeError extends Error {
|
|
|
48
99
|
if (typeof code !== 'string' || code.length === 0) {
|
|
49
100
|
throw new TypeError('RealtimeError code must be a non-empty string');
|
|
50
101
|
}
|
|
102
|
+
if (options?.detail !== undefined) {
|
|
103
|
+
assertDetail(options.detail);
|
|
104
|
+
}
|
|
51
105
|
super(message, options);
|
|
52
106
|
this.name = 'RealtimeError';
|
|
53
107
|
this.code = code;
|
|
108
|
+
this.detail = options?.detail;
|
|
54
109
|
}
|
|
55
110
|
}
|
|
56
111
|
//# sourceMappingURL=error.js.map
|
package/lib/error.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error.js","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAE3D,OAAO,KAAK,EACV,kBAAkB,EAClB,iBAAiB,EACjB,oBAAoB,EACpB,eAAe,EACf,uBAAuB,EACvB,sBAAsB,EACvB,MAAM,YAAY,CAAA;AAEnB,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAEhE,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf,QAAQ,EAAE,eAAe,CAAA;KAC1B;CACF;AAED;;;GAGG;AACH,MAAM,WAAW,yBAAyB;IACxC,6DAA6D;IAC7D,IAAI,IAAI,CAAA;IACR;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;CACnC;AAED;;;;;;GAMG;AACH,8BAAsB,eAAe;IACnC;;;;OAIG;IACH,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,oBAAoB,CAEnD;IAED;;;;;;;OAOG;IACH,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC,CAEnE;IAED;;;;;;;OAOG;IACH,QAAQ,CAAC,OAAO,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,eAAe,CAAC,CAAA;CAC5E;AAQD;;;;;GAKG;AACH,qBAAa,eAAgB,SAAQ,OAAO;IAC1C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyC;IAElE;;OAEG;IACH,YAAY,GAAG,EAAE,OAAO,EAEvB;IAED;;;;;;;;OAQG;IACH,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,eAAe,GAAG,yBAAyB,CAiCxF;IAED;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,OAAO;IAgCf;;;;;OAKG;IACH,OAAO,CAAC,MAAM;IASd;;;OAGG;IACH,aAAa,IAAI,oBAAoB,EAAE,CAEtC;IAED;;;;;OAKG;IACH,OAAO,CAAC,YAAY;IAQpB;;;;OAIG;IACG,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAe/D;IAED;;;;;;;;OAQG;IACG,OAAO,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,eAAe,CAAC,CAgBvE;IAED;;;;;;;;;;OAUG;IACH,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAWjE;CACF;AAED,qFAAqF;AACrF,YAAY,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,CAAA;eAE5C,eAAe"}
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/lib/types.d.ts
CHANGED
|
@@ -144,20 +144,32 @@ export interface RealtimeSession {
|
|
|
144
144
|
unmuteInput(): void;
|
|
145
145
|
/**
|
|
146
146
|
* Return a delegated result for the model to **speak aloud**.
|
|
147
|
+
*
|
|
148
|
+
* Resolves on the provider's **acknowledgement** when it answers a delegation. With no
|
|
149
|
+
* `delegationId` there is no acknowledgement to wait for: the provider accepts a session-wide append
|
|
150
|
+
* in silence, because it does not begin applying context at a bare `session.start`. That form
|
|
151
|
+
* therefore resolves on the write and is **best-effort** — it is applied once audio has flowed, and
|
|
152
|
+
* this seam cannot say when. It is not a way to speak unprompted.
|
|
147
153
|
* @param content - plain text, non-empty and within {@link MAX_APPEND_CHARS}.
|
|
148
|
-
* @param delegationId - the delegation this answers, or `undefined` for
|
|
154
|
+
* @param delegationId - the delegation this answers, or `undefined` for best-effort session context.
|
|
149
155
|
*/
|
|
150
156
|
appendCommentary(content: string, delegationId?: string): Promise<void>;
|
|
151
157
|
/**
|
|
152
158
|
* Add context the model may use **without speaking it** — progress, facts, intermediate state.
|
|
159
|
+
*
|
|
160
|
+
* With no `delegationId` this resolves on the write and is best-effort, for the reason recorded on
|
|
161
|
+
* {@link appendCommentary}: the provider does not acknowledge a session-wide append.
|
|
153
162
|
* @param content - plain text, non-empty and within {@link MAX_APPEND_CHARS}.
|
|
154
|
-
* @param delegationId - the delegation this relates to, or `undefined` for
|
|
163
|
+
* @param delegationId - the delegation this relates to, or `undefined` for best-effort session context.
|
|
155
164
|
*/
|
|
156
165
|
appendThinking(content: string, delegationId?: string): Promise<void>;
|
|
157
166
|
/**
|
|
158
167
|
* Steer the live conversation's behaviour (tone, brevity, policy) without speaking anything.
|
|
168
|
+
*
|
|
169
|
+
* With no `delegationId` this resolves on the write and is best-effort, for the reason recorded on
|
|
170
|
+
* {@link appendCommentary}.
|
|
159
171
|
* @param content - plain text, non-empty and within {@link MAX_APPEND_CHARS}.
|
|
160
|
-
* @param delegationId - `undefined` for session-wide steering.
|
|
172
|
+
* @param delegationId - `undefined` for best-effort session-wide steering.
|
|
161
173
|
*/
|
|
162
174
|
appendInstructions(content: string, delegationId?: string): Promise<void>;
|
|
163
175
|
/**
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/lib/types.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAkMH;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-realtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Realtime voice capability seam for DeepSeek Harness: a provider registry plus the session adapter base.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -18,15 +18,12 @@
|
|
|
18
18
|
"types": "./lib/error.d.ts",
|
|
19
19
|
"default": "./lib/error.js"
|
|
20
20
|
},
|
|
21
|
+
"./src/*": "./src/*",
|
|
21
22
|
"./package.json": "./package.json"
|
|
22
23
|
},
|
|
23
24
|
"files": [
|
|
24
|
-
"lib
|
|
25
|
-
"
|
|
26
|
-
"lib/types.js",
|
|
27
|
-
"lib/types.d.ts",
|
|
28
|
-
"lib/error.js",
|
|
29
|
-
"lib/error.d.ts"
|
|
25
|
+
"lib",
|
|
26
|
+
"src"
|
|
30
27
|
],
|
|
31
28
|
"license": "MIT",
|
|
32
29
|
"keywords": [
|
package/src/error.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed failures for the realtime seam, carrying stable machine codes.
|
|
3
|
+
*
|
|
4
|
+
* Codes are the seam's API: a consumer branches on `code`, never on message text. Adding a code is a
|
|
5
|
+
* minor release; changing what an existing code means is a breaking one.
|
|
6
|
+
*
|
|
7
|
+
* @module dsh-realtime/error
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Stable machine codes raised by this seam. */
|
|
11
|
+
export const REALTIME_ERROR_CODES = Object.freeze({
|
|
12
|
+
/** A registration named an empty or malformed provider route. */
|
|
13
|
+
INVALID_PROVIDER: 'INVALID_PROVIDER',
|
|
14
|
+
/** A route already has an adapter registered by another registration. */
|
|
15
|
+
DUPLICATE_PROVIDER: 'DUPLICATE_PROVIDER',
|
|
16
|
+
/** Registration was released, so it can no longer replace its routes. */
|
|
17
|
+
REGISTRATION_DISPOSED: 'REGISTRATION_DISPOSED',
|
|
18
|
+
/** No adapter is registered for the requested route. */
|
|
19
|
+
NO_ADAPTER: 'NO_ADAPTER',
|
|
20
|
+
/** An append was empty, not a string, or over the seam's bound. */
|
|
21
|
+
INVALID_APPEND: 'INVALID_APPEND',
|
|
22
|
+
/** The session has already been closed. */
|
|
23
|
+
SESSION_CLOSED: 'SESSION_CLOSED',
|
|
24
|
+
/** The credential an adapter needs is absent or unusable. Names the setting, never the value. */
|
|
25
|
+
MISSING_CREDENTIAL: 'MISSING_CREDENTIAL',
|
|
26
|
+
/** The provider reported a failure, or an operation it was expected to acknowledge never was. */
|
|
27
|
+
PROVIDER_ERROR: 'PROVIDER_ERROR',
|
|
28
|
+
/** A recorded session could not be read as a recording. */
|
|
29
|
+
INVALID_RECORDING: 'INVALID_RECORDING',
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------------------------
|
|
32
|
+
// Failure taxonomy. These exist so a consumer can branch on the *class* of a failure rather than
|
|
33
|
+
// parse prose: a setup state, a rejected credential, an account that cannot pay, and a throttle
|
|
34
|
+
// need four different responses, and collapsing them is what makes onboarding feel unfinished.
|
|
35
|
+
// ---------------------------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The capability is not configured yet — an expected state on a fresh install, not a fault.
|
|
39
|
+
*
|
|
40
|
+
* Distinct from {@link REALTIME_ERROR_CODES.CREDENTIAL_REJECTED}: absent is fixed by supplying a
|
|
41
|
+
* value, unusable by replacing one.
|
|
42
|
+
*/
|
|
43
|
+
NOT_CONFIGURED: 'NOT_CONFIGURED',
|
|
44
|
+
/** A credential is present and the provider refused it. Replace it; retrying cannot help. */
|
|
45
|
+
CREDENTIAL_REJECTED: 'CREDENTIAL_REJECTED',
|
|
46
|
+
/** The credential is valid but the account cannot use what was requested (model, region, scope). */
|
|
47
|
+
NOT_ENTITLED: 'NOT_ENTITLED',
|
|
48
|
+
/** The account cannot pay for the request — an exhausted balance or a hard billing limit. */
|
|
49
|
+
INSUFFICIENT_CREDIT: 'INSUFFICIENT_CREDIT',
|
|
50
|
+
/** The provider throttled the request. Repeating later may succeed. */
|
|
51
|
+
RATE_LIMITED: 'RATE_LIMITED',
|
|
52
|
+
/** The provider was asked something and did not answer within the bound. */
|
|
53
|
+
PROVIDER_TIMEOUT: 'PROVIDER_TIMEOUT',
|
|
54
|
+
/** The transport failed — a socket error, not a decision the provider made. */
|
|
55
|
+
NETWORK: 'NETWORK',
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
/** One of {@link REALTIME_ERROR_CODES}. */
|
|
59
|
+
export type RealtimeErrorCode = (typeof REALTIME_ERROR_CODES)[keyof typeof REALTIME_ERROR_CODES]
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* What a caller can act on, carried as structure rather than prose.
|
|
63
|
+
*
|
|
64
|
+
* A failure a user cannot act on is a failure report that wasted their time. Every field here exists
|
|
65
|
+
* so the *class* of a failure survives translation: an agent can relay `remedy` verbatim, a settings
|
|
66
|
+
* surface can highlight `setting`, a retry loop can consult `retryable`, and a human reading a log
|
|
67
|
+
* can see the provider's own `providerCode` instead of a paraphrase of it.
|
|
68
|
+
*
|
|
69
|
+
* Never carries secret material. `providerCode` is the provider's own error code, never a credential.
|
|
70
|
+
*/
|
|
71
|
+
export interface RealtimeFailureDetail {
|
|
72
|
+
/**
|
|
73
|
+
* What to do about it, written to be relayed **verbatim** to whoever is trying to use the feature.
|
|
74
|
+
* One sentence, imperative, no jargon.
|
|
75
|
+
*/
|
|
76
|
+
remedy?: string
|
|
77
|
+
/** The configuration key that must be supplied, when the failure is a configuration state. */
|
|
78
|
+
setting?: string
|
|
79
|
+
/** Whether repeating the identical call could succeed without any change. */
|
|
80
|
+
retryable?: boolean
|
|
81
|
+
/** The provider's own error code, verbatim. Never a translated or inferred value. */
|
|
82
|
+
providerCode?: string
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Expected `typeof` for each optional detail field. Data, so the check costs one loop and not a branch each. */
|
|
86
|
+
const DETAIL_FIELD_TYPES = Object.freeze({
|
|
87
|
+
remedy: 'string',
|
|
88
|
+
setting: 'string',
|
|
89
|
+
providerCode: 'string',
|
|
90
|
+
retryable: 'boolean',
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Reject a malformed detail before it is attached to a failure.
|
|
95
|
+
* @param detail - candidate detail.
|
|
96
|
+
* @throws TypeError naming the offending field.
|
|
97
|
+
*/
|
|
98
|
+
function assertDetail(detail: RealtimeFailureDetail): void {
|
|
99
|
+
for (const [field, expected] of Object.entries(DETAIL_FIELD_TYPES)) {
|
|
100
|
+
const value = (detail as Record<string, unknown>)[field]
|
|
101
|
+
if (value === undefined) continue
|
|
102
|
+
if (typeof value !== expected) {
|
|
103
|
+
throw new TypeError(`RealtimeError detail.${field} must be a ${expected}`)
|
|
104
|
+
}
|
|
105
|
+
if (expected === 'string' && value === '') {
|
|
106
|
+
throw new TypeError(`RealtimeError detail.${field} must be non-empty when supplied`)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* A typed seam failure.
|
|
113
|
+
*
|
|
114
|
+
* The constructor validates its own arguments rather than trusting callers: a failure raised while
|
|
115
|
+
* reporting a failure is the worst place to discover a malformed argument.
|
|
116
|
+
*/
|
|
117
|
+
export class RealtimeError extends Error {
|
|
118
|
+
/** Stable machine code. Branch on this, never on `message`. */
|
|
119
|
+
readonly code: RealtimeErrorCode
|
|
120
|
+
|
|
121
|
+
/** What a caller can act on, when the failure is one they can act on. */
|
|
122
|
+
readonly detail: RealtimeFailureDetail | undefined
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @param message - non-empty human-readable summary. Must not contain secret material.
|
|
126
|
+
* @param code - one of {@link REALTIME_ERROR_CODES}.
|
|
127
|
+
* @param options - optional `cause`, and an optional structured `detail`.
|
|
128
|
+
*/
|
|
129
|
+
constructor(message: string, code: RealtimeErrorCode, options?: ErrorOptions & { detail?: RealtimeFailureDetail }) {
|
|
130
|
+
if (typeof message !== 'string' || message.length === 0) {
|
|
131
|
+
throw new TypeError('RealtimeError message must be a non-empty string')
|
|
132
|
+
}
|
|
133
|
+
if (typeof code !== 'string' || code.length === 0) {
|
|
134
|
+
throw new TypeError('RealtimeError code must be a non-empty string')
|
|
135
|
+
}
|
|
136
|
+
if (options?.detail !== undefined) {
|
|
137
|
+
assertDetail(options.detail)
|
|
138
|
+
}
|
|
139
|
+
super(message, options)
|
|
140
|
+
this.name = 'RealtimeError'
|
|
141
|
+
this.code = code
|
|
142
|
+
this.detail = options?.detail
|
|
143
|
+
}
|
|
144
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Realtime voice seam: a provider registry plus the adapter base every voice backend extends.
|
|
3
|
+
*
|
|
4
|
+
* Exports the `RealtimeRuntime` service as the **default** export (a DeepSeek Harness convention for
|
|
5
|
+
* service packages) and the abstract `RealtimeAdapter` for provider backends. Function plugins
|
|
6
|
+
* must supply `name` / `inject` / `Config` / `apply` and no default export; this package is a
|
|
7
|
+
* service package, so it is mounted as a class plugin and default-exports its service.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-realtime
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Service, type Context } from '@deepseek-ai/cordis'
|
|
13
|
+
import { REALTIME_ERROR_CODES, RealtimeError } from './error.ts'
|
|
14
|
+
import type {
|
|
15
|
+
RealtimeDelegation,
|
|
16
|
+
RealtimeModelInfo,
|
|
17
|
+
RealtimeProviderInfo,
|
|
18
|
+
RealtimeSession,
|
|
19
|
+
RealtimeSessionHandlers,
|
|
20
|
+
RealtimeSessionOptions,
|
|
21
|
+
} from './types.ts'
|
|
22
|
+
|
|
23
|
+
export * from './types.ts'
|
|
24
|
+
export * from './error.ts'
|
|
25
|
+
export { RealtimeError, REALTIME_ERROR_CODES } from './error.ts'
|
|
26
|
+
|
|
27
|
+
declare module '@deepseek-ai/cordis' {
|
|
28
|
+
interface Context {
|
|
29
|
+
realtime: RealtimeRuntime
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* What {@link RealtimeRuntime.registerAdapter} returns: the disposer, plus an atomic route
|
|
35
|
+
* replacement for the same adapter instance.
|
|
36
|
+
*/
|
|
37
|
+
export interface AdapterRegistrationHandle {
|
|
38
|
+
/** Release every route this registration currently holds. */
|
|
39
|
+
(): void
|
|
40
|
+
/**
|
|
41
|
+
* Replace this registration's routes, keeping the same adapter instance. The candidate set is
|
|
42
|
+
* validated in full first — a conflict with another registration or a malformed route throws and
|
|
43
|
+
* leaves the current routes untouched — and the swap is one synchronous section, so no observer
|
|
44
|
+
* can see the registry between release and re-registration.
|
|
45
|
+
*
|
|
46
|
+
* An empty array is legal here (a plugin whose configuration emptied holds zero routes while
|
|
47
|
+
* staying registered), unlike an empty initial registration.
|
|
48
|
+
*
|
|
49
|
+
* Throws `REGISTRATION_DISPOSED` once the registration was released: its routes are gone and its
|
|
50
|
+
* disposer has already run, so anything registered afterwards would have no owner left to release it.
|
|
51
|
+
* @param providers - the complete next route set for this registration.
|
|
52
|
+
*/
|
|
53
|
+
replace(providers: string[]): void
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Provider-wire adapter for the realtime session vocabulary.
|
|
58
|
+
*
|
|
59
|
+
* Register implementations with `ctx.realtime.registerAdapter(providers, adapter)`. The single
|
|
60
|
+
* required method is {@link session}; every other method exists so a provider can describe itself
|
|
61
|
+
* without the seam having to special-case it.
|
|
62
|
+
*/
|
|
63
|
+
export abstract class RealtimeAdapter {
|
|
64
|
+
/**
|
|
65
|
+
* Describe one provider route owned by this adapter.
|
|
66
|
+
* @param provider - a route passed to `registerAdapter()` for this instance.
|
|
67
|
+
* @returns detached display metadata whose `id` must equal `provider`.
|
|
68
|
+
*/
|
|
69
|
+
providerInfo(provider: string): RealtimeProviderInfo {
|
|
70
|
+
return { id: provider, name: provider }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* List the voice models this adapter can currently advertise for one owned route.
|
|
75
|
+
*
|
|
76
|
+
* The result is advisory: an adapter may accept unlisted model ids, and consumers must not turn
|
|
77
|
+
* absence into request rejection.
|
|
78
|
+
* @param _provider - one provider route owned by this adapter.
|
|
79
|
+
* @returns discoverable models in adapter-preferred order.
|
|
80
|
+
*/
|
|
81
|
+
listModels(_provider: string): Promise<readonly RealtimeModelInfo[]> {
|
|
82
|
+
return Promise.resolve([])
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Open one voice session. The only required method.
|
|
87
|
+
*
|
|
88
|
+
* Implementations must honor `options.signal` during establishment, and must throw (rather than
|
|
89
|
+
* resolve with a dead session) when the session cannot open.
|
|
90
|
+
* @param options - the fully-resolved request; `options.provider` selects the registered route.
|
|
91
|
+
* @returns the live session, with `options.handlers` already wired.
|
|
92
|
+
*/
|
|
93
|
+
abstract session(options: RealtimeSessionOptions): Promise<RealtimeSession>
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** One resolved route registration. */
|
|
97
|
+
interface AdapterRegistration {
|
|
98
|
+
readonly adapter: RealtimeAdapter
|
|
99
|
+
readonly provider: RealtimeProviderInfo
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The `realtime` service: an adapter registry over the session vocabulary.
|
|
104
|
+
*
|
|
105
|
+
* Registration is effect-based, so HMR or a disposal unmounts routes with the contributing fiber —
|
|
106
|
+
* there is no separate teardown path that can be forgotten.
|
|
107
|
+
*/
|
|
108
|
+
export class RealtimeRuntime extends Service {
|
|
109
|
+
private readonly adapters = new Map<string, AdapterRegistration>()
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @param ctx - the Cordis context this service is mounted on.
|
|
113
|
+
*/
|
|
114
|
+
constructor(ctx: Context) {
|
|
115
|
+
super(ctx, 'realtime')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Register an adapter for the given provider routes, all-or-nothing.
|
|
120
|
+
*
|
|
121
|
+
* Disposed with the fiber. Throws `INVALID_PROVIDER` for a malformed route, `DUPLICATE_PROVIDER`
|
|
122
|
+
* if any route is already held by another registration.
|
|
123
|
+
* @param providers - every provider route this adapter should serve.
|
|
124
|
+
* @param adapter - the adapter that opens sessions for those routes.
|
|
125
|
+
* @returns the disposer, carrying {@link AdapterRegistrationHandle.replace}.
|
|
126
|
+
*/
|
|
127
|
+
registerAdapter(providers: string[], adapter: RealtimeAdapter): AdapterRegistrationHandle {
|
|
128
|
+
if (providers.length === 0) {
|
|
129
|
+
throw new RealtimeError('an adapter must register at least one provider', REALTIME_ERROR_CODES.INVALID_PROVIDER)
|
|
130
|
+
}
|
|
131
|
+
// Routes this registration currently holds; `replace` rewrites it, and the disposer releases
|
|
132
|
+
// whatever it holds at disposal time.
|
|
133
|
+
const owned = new Set<string>()
|
|
134
|
+
// `owned` being empty cannot report disposal on its own, because `replace([])` legally leaves a
|
|
135
|
+
// live registration holding none.
|
|
136
|
+
let released = false
|
|
137
|
+
|
|
138
|
+
const dispose = this.ctx.effect(function* (this: RealtimeRuntime) {
|
|
139
|
+
this.commit(owned, this.prepare(providers, adapter, owned))
|
|
140
|
+
yield () => {
|
|
141
|
+
released = true
|
|
142
|
+
for (const provider of owned) this.adapters.delete(provider)
|
|
143
|
+
owned.clear()
|
|
144
|
+
}
|
|
145
|
+
}.bind(this), 'realtime.registerAdapter()')
|
|
146
|
+
|
|
147
|
+
const handle = (() => void dispose()) as AdapterRegistrationHandle
|
|
148
|
+
handle.replace = (next: string[]): void => {
|
|
149
|
+
// Registering here would leak: the effect's disposer already ran, so nothing remains to
|
|
150
|
+
// release whatever this call would put in the map.
|
|
151
|
+
if (released) {
|
|
152
|
+
throw new RealtimeError(
|
|
153
|
+
'a disposed adapter registration cannot replace its routes',
|
|
154
|
+
REALTIME_ERROR_CODES.REGISTRATION_DISPOSED,
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
this.commit(owned, this.prepare(next, adapter, owned))
|
|
158
|
+
}
|
|
159
|
+
return handle
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Validate one candidate route set for `adapter`, treating routes this registration already holds
|
|
164
|
+
* as available.
|
|
165
|
+
*
|
|
166
|
+
* Nothing is mutated: a rejected candidate leaves the registry exactly as it was, which is what
|
|
167
|
+
* makes {@link AdapterRegistrationHandle.replace} a swap rather than a delete-then-add that can
|
|
168
|
+
* strand the registry empty.
|
|
169
|
+
* @param providers - candidate routes.
|
|
170
|
+
* @param adapter - the adapter that would own them.
|
|
171
|
+
* @param owned - routes this same registration already holds.
|
|
172
|
+
* @returns registrations ready to commit.
|
|
173
|
+
*/
|
|
174
|
+
private prepare(providers: string[], adapter: RealtimeAdapter, owned: ReadonlySet<string>): AdapterRegistration[] {
|
|
175
|
+
const unique = new Set<string>()
|
|
176
|
+
const registrations: AdapterRegistration[] = []
|
|
177
|
+
for (const provider of providers) {
|
|
178
|
+
if (typeof provider !== 'string' || provider.length === 0) {
|
|
179
|
+
throw new RealtimeError('adapter provider names must be non-empty strings', REALTIME_ERROR_CODES.INVALID_PROVIDER)
|
|
180
|
+
}
|
|
181
|
+
if (unique.has(provider) || (this.adapters.has(provider) && !owned.has(provider))) {
|
|
182
|
+
throw new RealtimeError(
|
|
183
|
+
`an adapter for provider "${provider}" is already registered`,
|
|
184
|
+
REALTIME_ERROR_CODES.DUPLICATE_PROVIDER,
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
const info = adapter.providerInfo(provider)
|
|
188
|
+
if (typeof info.id !== 'string' || info.id !== provider
|
|
189
|
+
|| typeof info.name !== 'string' || info.name.length === 0) {
|
|
190
|
+
throw new RealtimeError(
|
|
191
|
+
`adapter metadata for provider "${provider}" must preserve its id and have a non-empty name`,
|
|
192
|
+
REALTIME_ERROR_CODES.INVALID_PROVIDER,
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
unique.add(provider)
|
|
196
|
+
registrations.push({
|
|
197
|
+
adapter,
|
|
198
|
+
provider: info.description === undefined
|
|
199
|
+
? { id: info.id, name: info.name }
|
|
200
|
+
: { id: info.id, name: info.name, description: info.description },
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
return registrations
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Swap this registration's routes for the prepared ones in one synchronous section, so no
|
|
208
|
+
* observer can see the registry between the release and the re-registration.
|
|
209
|
+
* @param owned - mutable set tracking which routes this registration holds.
|
|
210
|
+
* @param registrations - validated registrations to install.
|
|
211
|
+
*/
|
|
212
|
+
private commit(owned: Set<string>, registrations: readonly AdapterRegistration[]): void {
|
|
213
|
+
for (const provider of owned) this.adapters.delete(provider)
|
|
214
|
+
owned.clear()
|
|
215
|
+
for (const registration of registrations) {
|
|
216
|
+
this.adapters.set(registration.provider.id, registration)
|
|
217
|
+
owned.add(registration.provider.id)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Describe the provider routes that currently have an adapter.
|
|
223
|
+
* @returns detached provider metadata in registration order.
|
|
224
|
+
*/
|
|
225
|
+
listProviders(): RealtimeProviderInfo[] {
|
|
226
|
+
return [...this.adapters.values()].map(({ provider }) => ({ ...provider }))
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Resolve the adapter owning one route.
|
|
231
|
+
* @param provider - registered route to look up.
|
|
232
|
+
* @returns that route's registration.
|
|
233
|
+
* @throws RealtimeError `NO_ADAPTER` when the route is unregistered.
|
|
234
|
+
*/
|
|
235
|
+
private registration(provider: string): AdapterRegistration {
|
|
236
|
+
const registration = this.adapters.get(provider)
|
|
237
|
+
if (registration === undefined) {
|
|
238
|
+
throw new RealtimeError(`no realtime adapter registered for provider "${provider}"`, REALTIME_ERROR_CODES.NO_ADAPTER)
|
|
239
|
+
}
|
|
240
|
+
return registration
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Discover the voice models one registered route advertises.
|
|
245
|
+
* @param provider - registered route to inspect.
|
|
246
|
+
* @returns detached model metadata in adapter-preferred order, duplicates removed.
|
|
247
|
+
*/
|
|
248
|
+
async listModels(provider: string): Promise<RealtimeModelInfo[]> {
|
|
249
|
+
const models = await this.registration(provider).adapter.listModels(provider)
|
|
250
|
+
const seen = new Set<string>()
|
|
251
|
+
const detached: RealtimeModelInfo[] = []
|
|
252
|
+
for (const model of models) {
|
|
253
|
+
if (typeof model.id !== 'string' || model.id.length === 0 || seen.has(model.id)) continue
|
|
254
|
+
seen.add(model.id)
|
|
255
|
+
detached.push({
|
|
256
|
+
id: model.id,
|
|
257
|
+
name: typeof model.name === 'string' && model.name.length > 0 ? model.name : model.id,
|
|
258
|
+
...model.inputModalities === undefined ? {} : { inputModalities: [...model.inputModalities] },
|
|
259
|
+
...model.outputModalities === undefined ? {} : { outputModalities: [...model.outputModalities] },
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
return detached
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Open one voice session through the adapter registered for its route.
|
|
267
|
+
*
|
|
268
|
+
* A field this seam cannot honor is rejected here rather than forwarded as a no-op — the caller
|
|
269
|
+
* learns the request is unsupported before a live conversation depends on it.
|
|
270
|
+
* @param options - the session request; `options.provider` selects the adapter.
|
|
271
|
+
* @returns the adapter's live session.
|
|
272
|
+
* @throws RealtimeError `NO_ADAPTER` for an unregistered route.
|
|
273
|
+
*/
|
|
274
|
+
async session(options: RealtimeSessionOptions): Promise<RealtimeSession> {
|
|
275
|
+
if (typeof options.provider !== 'string' || options.provider.length === 0) {
|
|
276
|
+
throw new RealtimeError('a session needs a non-empty provider route', REALTIME_ERROR_CODES.INVALID_PROVIDER)
|
|
277
|
+
}
|
|
278
|
+
if (typeof options.model !== 'string' || options.model.length === 0) {
|
|
279
|
+
throw new RealtimeError('a session needs a non-empty model id', REALTIME_ERROR_CODES.INVALID_PROVIDER)
|
|
280
|
+
}
|
|
281
|
+
if (options.instructions !== undefined && options.instructions.length === 0) {
|
|
282
|
+
throw new RealtimeError('session instructions must be non-empty when supplied', REALTIME_ERROR_CODES.INVALID_APPEND)
|
|
283
|
+
}
|
|
284
|
+
if (options.signal?.aborted) {
|
|
285
|
+
throw new RealtimeError('session establishment was aborted before it started', REALTIME_ERROR_CODES.SESSION_CLOSED, {
|
|
286
|
+
cause: options.signal.reason,
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
return await this.registration(options.provider).adapter.session(options)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Validate one context append against the seam's bounds.
|
|
294
|
+
*
|
|
295
|
+
* Exposed so an adapter can enforce the same bound the seam promises, instead of each backend
|
|
296
|
+
* re-deriving it. Bound enforcement lives at the operation that makes the decision: a caller that
|
|
297
|
+
* bypasses this cannot silently send an over-long append.
|
|
298
|
+
* @param content - candidate append text.
|
|
299
|
+
* @param maxChars - character ceiling; defaults to the provider's documented bound.
|
|
300
|
+
* @returns the content unchanged, for convenient inline use.
|
|
301
|
+
* @throws RealtimeError `INVALID_APPEND` for a non-string, empty, or over-long value.
|
|
302
|
+
*/
|
|
303
|
+
static assertAppendable(content: string, maxChars: number): string {
|
|
304
|
+
if (typeof content !== 'string' || content.length === 0) {
|
|
305
|
+
throw new RealtimeError('an append needs non-empty content', REALTIME_ERROR_CODES.INVALID_APPEND)
|
|
306
|
+
}
|
|
307
|
+
if (content.length > maxChars) {
|
|
308
|
+
throw new RealtimeError(
|
|
309
|
+
`an append of ${content.length} characters exceeds the ${maxChars}-character seam bound`,
|
|
310
|
+
REALTIME_ERROR_CODES.INVALID_APPEND,
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
return content
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Re-exported so adapters can type their handler wiring without a second import. */
|
|
318
|
+
export type { RealtimeDelegation, RealtimeSessionHandlers }
|
|
319
|
+
|
|
320
|
+
export default RealtimeRuntime
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the realtime voice seam. This module contains **no runtime code** — a
|
|
3
|
+
* DeepSeek Harness convention that keeps type-only imports erasable.
|
|
4
|
+
*
|
|
5
|
+
* @module dsh-realtime/types
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** A model route a realtime adapter can serve. */
|
|
9
|
+
export interface RealtimeProviderInfo {
|
|
10
|
+
/** Route id a caller passes as `RealtimeSessionOptions.provider`. Must equal the registered route. */
|
|
11
|
+
id: string
|
|
12
|
+
/** Human-readable label for diagnostics and surfaces. */
|
|
13
|
+
name: string
|
|
14
|
+
/** Optional one-line description of what this route speaks. */
|
|
15
|
+
description?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** One voice model advertised by an adapter. Catalog membership is advisory: it never gates a session. */
|
|
19
|
+
export interface RealtimeModelInfo {
|
|
20
|
+
/** Exact model id passed as `RealtimeSessionOptions.model`. */
|
|
21
|
+
id: string
|
|
22
|
+
/** Human-readable label. */
|
|
23
|
+
name: string
|
|
24
|
+
/** What the model can take in, e.g. `audio`, `text`, `image`. */
|
|
25
|
+
inputModalities?: readonly RealtimeModality[]
|
|
26
|
+
/** What the model puts out, e.g. `audio`, `text`. */
|
|
27
|
+
outputModalities?: readonly RealtimeModality[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A modality a realtime model accepts or emits. */
|
|
31
|
+
export type RealtimeModality = 'audio' | 'text' | 'image'
|
|
32
|
+
|
|
33
|
+
/** Raw audio encoding expected on the wire. */
|
|
34
|
+
export interface RealtimeAudioFormat {
|
|
35
|
+
/** Sample rate in Hz. `24000` for GPT-Live-1. */
|
|
36
|
+
sampleRate: number
|
|
37
|
+
/** Channel count. `1` (mono) for GPT-Live-1. */
|
|
38
|
+
channels: number
|
|
39
|
+
/** Sample encoding. `pcm16` is signed little-endian 16-bit. */
|
|
40
|
+
encoding: 'pcm16'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** What a caller asks an adapter to open. */
|
|
44
|
+
export interface RealtimeSessionOptions {
|
|
45
|
+
/** Registered route to use. Selects the adapter. */
|
|
46
|
+
provider: string
|
|
47
|
+
/** Exact model id. */
|
|
48
|
+
model: string
|
|
49
|
+
/**
|
|
50
|
+
* Opening system instructions.
|
|
51
|
+
*
|
|
52
|
+
* Advisory at the seam: the protocol may treat these as immutable once a session starts, in which
|
|
53
|
+
* case an adapter must surface a coded error rather than silently dropping the request. Extending
|
|
54
|
+
* live instructions is `RealtimeSession.appendInstructions`, not a new session.
|
|
55
|
+
*/
|
|
56
|
+
instructions?: string
|
|
57
|
+
/** Output voice id. Provider-specific; `undefined` leaves the provider default. */
|
|
58
|
+
voice?: string
|
|
59
|
+
/** Cancellation for session establishment. Implementations must settle promptly after it aborts. */
|
|
60
|
+
signal?: AbortSignal
|
|
61
|
+
/** Handler callbacks for the life of the session. */
|
|
62
|
+
handlers?: RealtimeSessionHandlers
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A delegated unit of work handed to the application by the voice model. */
|
|
66
|
+
export interface RealtimeDelegation {
|
|
67
|
+
/** Opaque correlation id. Preserve it unchanged on every update about this delegation. */
|
|
68
|
+
id: string
|
|
69
|
+
/** Who is expected to do the work. `client` means this application. */
|
|
70
|
+
target: 'client' | 'responses'
|
|
71
|
+
/**
|
|
72
|
+
* Position on the session timeline, in milliseconds.
|
|
73
|
+
*
|
|
74
|
+
* Note there is deliberately **no task text** here: the protocol sends metadata only, so a
|
|
75
|
+
* consumer reconstructs intent from transcripts plus its own application state.
|
|
76
|
+
*/
|
|
77
|
+
offsetMs: number
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** A transcript fragment from either side of the conversation. */
|
|
81
|
+
export interface RealtimeTranscript {
|
|
82
|
+
/** Which side spoke. */
|
|
83
|
+
kind: 'input' | 'output'
|
|
84
|
+
/** The fragment text. Deltas concatenate in arrival order. */
|
|
85
|
+
text: string
|
|
86
|
+
/** Whether this fragment closes the utterance. */
|
|
87
|
+
final: boolean
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Cumulative session usage. The protocol reports this in audio-seconds, not tokens. */
|
|
91
|
+
export interface RealtimeUsage {
|
|
92
|
+
/** Cumulative audio seconds billed for this session. */
|
|
93
|
+
seconds: number
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Callbacks an adapter invokes for session lifetime events. */
|
|
97
|
+
export interface RealtimeSessionHandlers {
|
|
98
|
+
/** The session is established and can accept audio. */
|
|
99
|
+
onReady?(info: RealtimeSessionStarted): void
|
|
100
|
+
/** A transcript fragment arrived. */
|
|
101
|
+
onTranscript?(transcript: RealtimeTranscript): void
|
|
102
|
+
/** The model delegated work to this application. */
|
|
103
|
+
onDelegation?(delegation: RealtimeDelegation): void
|
|
104
|
+
/** Cumulative usage was updated. */
|
|
105
|
+
onUsage?(usage: RealtimeUsage): void
|
|
106
|
+
/** Raw output audio, already decoded from the wire encoding. */
|
|
107
|
+
onAudio?(pcm16: Uint8Array): void
|
|
108
|
+
/** The session ended. `reason` is provider-supplied when available. */
|
|
109
|
+
onClosed?(reason?: string): void
|
|
110
|
+
/**
|
|
111
|
+
* A session-scoped failure the adapter contained rather than throwing.
|
|
112
|
+
*
|
|
113
|
+
* Adapters report here for failures that occur *after* the session opened; failures that prevent
|
|
114
|
+
* the session opening are thrown from `session()`.
|
|
115
|
+
*/
|
|
116
|
+
onError?(error: Error): void
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Facts about an established session. */
|
|
120
|
+
export interface RealtimeSessionStarted {
|
|
121
|
+
/** The route that opened it. */
|
|
122
|
+
provider: string
|
|
123
|
+
/** The model the provider accepted — which may differ from the requested id if the provider aliases. */
|
|
124
|
+
model: string
|
|
125
|
+
/** The output voice the provider accepted. */
|
|
126
|
+
voice?: string
|
|
127
|
+
/** Audio encoding the session expects for input. */
|
|
128
|
+
inputAudio: RealtimeAudioFormat
|
|
129
|
+
/** Audio encoding the session emits. */
|
|
130
|
+
outputAudio: RealtimeAudioFormat
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* A live voice session.
|
|
135
|
+
*
|
|
136
|
+
* Implementations own the transport. Callers own the conversation: they push audio in, receive
|
|
137
|
+
* callbacks, and answer delegations through the append methods.
|
|
138
|
+
*/
|
|
139
|
+
export interface RealtimeSession {
|
|
140
|
+
/** Provider-assigned session identifier, for correlation in logs. */
|
|
141
|
+
readonly id: string
|
|
142
|
+
/** Facts the provider accepted at startup. */
|
|
143
|
+
readonly started: RealtimeSessionStarted
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Append one frame of microphone audio.
|
|
147
|
+
*
|
|
148
|
+
* There is deliberately **no end-of-utterance call**: endpointing belongs to the provider, and a
|
|
149
|
+
* client-side detector would be a second, competing turn boundary. Frames are the only input.
|
|
150
|
+
* @param pcm16 - raw samples in the session's `inputAudio` format.
|
|
151
|
+
*/
|
|
152
|
+
sendAudio(pcm16: Uint8Array): void
|
|
153
|
+
|
|
154
|
+
/** Stop the provider consuming microphone audio, without ending the session. */
|
|
155
|
+
muteInput(): void
|
|
156
|
+
|
|
157
|
+
/** Resume microphone consumption after {@link muteInput}. */
|
|
158
|
+
unmuteInput(): void
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Return a delegated result for the model to **speak aloud**.
|
|
162
|
+
*
|
|
163
|
+
* Resolves on the provider's **acknowledgement** when it answers a delegation. With no
|
|
164
|
+
* `delegationId` there is no acknowledgement to wait for: the provider accepts a session-wide append
|
|
165
|
+
* in silence, because it does not begin applying context at a bare `session.start`. That form
|
|
166
|
+
* therefore resolves on the write and is **best-effort** — it is applied once audio has flowed, and
|
|
167
|
+
* this seam cannot say when. It is not a way to speak unprompted.
|
|
168
|
+
* @param content - plain text, non-empty and within {@link MAX_APPEND_CHARS}.
|
|
169
|
+
* @param delegationId - the delegation this answers, or `undefined` for best-effort session context.
|
|
170
|
+
*/
|
|
171
|
+
appendCommentary(content: string, delegationId?: string): Promise<void>
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Add context the model may use **without speaking it** — progress, facts, intermediate state.
|
|
175
|
+
*
|
|
176
|
+
* With no `delegationId` this resolves on the write and is best-effort, for the reason recorded on
|
|
177
|
+
* {@link appendCommentary}: the provider does not acknowledge a session-wide append.
|
|
178
|
+
* @param content - plain text, non-empty and within {@link MAX_APPEND_CHARS}.
|
|
179
|
+
* @param delegationId - the delegation this relates to, or `undefined` for best-effort session context.
|
|
180
|
+
*/
|
|
181
|
+
appendThinking(content: string, delegationId?: string): Promise<void>
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Steer the live conversation's behaviour (tone, brevity, policy) without speaking anything.
|
|
185
|
+
*
|
|
186
|
+
* With no `delegationId` this resolves on the write and is best-effort, for the reason recorded on
|
|
187
|
+
* {@link appendCommentary}.
|
|
188
|
+
* @param content - plain text, non-empty and within {@link MAX_APPEND_CHARS}.
|
|
189
|
+
* @param delegationId - `undefined` for best-effort session-wide steering.
|
|
190
|
+
*/
|
|
191
|
+
appendInstructions(content: string, delegationId?: string): Promise<void>
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* End the session and release the transport. Idempotent.
|
|
195
|
+
* @returns a promise settling once the transport is released.
|
|
196
|
+
*/
|
|
197
|
+
close(): Promise<void>
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Upper bound, in characters, on one context append.
|
|
202
|
+
*
|
|
203
|
+
* The provider's real limit is **500 tokens**, which this seam cannot count without binding to a
|
|
204
|
+
* tokenizer it does not own. The character ceiling is therefore a deliberate conservative proxy:
|
|
205
|
+
* it is enforced here so that an over-long append fails locally with a coded error rather than
|
|
206
|
+
* halfway through a live conversation. Exact token accounting belongs to the adapter.
|
|
207
|
+
*/
|
|
208
|
+
export const MAX_APPEND_CHARS = 2000
|