@dialt/sdk 0.23.0 → 0.24.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/CHANGELOG.md +5 -0
- package/README.md +11 -11
- package/package.json +1 -1
- package/src/ambience.js +1 -1
- package/src/index.js +29 -25
- package/src/mic.js +1 -1
- package/src/webrtc.js +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.24.0
|
|
4
|
+
|
|
5
|
+
- `DialtClient` is now the canonical browser client name; `ConverseClient` remains as a deprecated source-compatible alias.
|
|
6
|
+
- New sessions send `mode.kind: 'dialt'`; legacy `converse` input is still accepted and normalized.
|
|
7
|
+
|
|
3
8
|
## 0.23.0
|
|
4
9
|
|
|
5
10
|
- The package is now published as `@dialt/sdk`; `@trelis/converse` is deprecated.
|
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Keep your persistent `ck_…` API key on your backend. Exchange it for a scoped
|
|
|
12
12
|
with `POST /api/v1/session-keys`, then connect from a user gesture:
|
|
13
13
|
|
|
14
14
|
```js
|
|
15
|
-
import {
|
|
15
|
+
import { DialtClient } from '@dialt/sdk';
|
|
16
16
|
|
|
17
17
|
const credential = await fetch('/voice/session', {
|
|
18
18
|
method: 'POST',
|
|
@@ -22,11 +22,11 @@ const credential = await fetch('/voice/session', {
|
|
|
22
22
|
return response.json();
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
const client = new
|
|
25
|
+
const client = new DialtClient({
|
|
26
26
|
url: 'wss://dialt.com/ws',
|
|
27
27
|
sessionId: credential.session_id,
|
|
28
28
|
apiKey: credential.api_key,
|
|
29
|
-
mode: { kind: '
|
|
29
|
+
mode: { kind: 'dialt' },
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
startButton.addEventListener('click', async () => {
|
|
@@ -113,7 +113,7 @@ token. To preserve the same bounded resume opportunity across a full page reload
|
|
|
113
113
|
opaque state in tab-scoped storage and import it into the replacement client:
|
|
114
114
|
|
|
115
115
|
```js
|
|
116
|
-
const storageKey = '
|
|
116
|
+
const storageKey = 'dialt-resume-state';
|
|
117
117
|
const clientOptions = {
|
|
118
118
|
url: 'wss://dialt.com/ws',
|
|
119
119
|
sessionId: credential.session_id,
|
|
@@ -128,7 +128,7 @@ try {
|
|
|
128
128
|
sessionStorage.removeItem(storageKey);
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
const client = new
|
|
131
|
+
const client = new DialtClient(clientOptions);
|
|
132
132
|
if (savedResumeState !== null) {
|
|
133
133
|
try {
|
|
134
134
|
client.importResumeState(savedResumeState);
|
|
@@ -159,11 +159,11 @@ Text sessions use the ordinary Dialt model, instructions, tools and events witho
|
|
|
159
159
|
microphone or audio pipeline:
|
|
160
160
|
|
|
161
161
|
```js
|
|
162
|
-
const textClient = new
|
|
162
|
+
const textClient = new DialtClient({
|
|
163
163
|
url: 'wss://dialt.com/ws',
|
|
164
164
|
sessionId: credential.session_id,
|
|
165
165
|
apiKey: credential.api_key,
|
|
166
|
-
mode: { kind: '
|
|
166
|
+
mode: { kind: 'dialt', modality: 'text' },
|
|
167
167
|
});
|
|
168
168
|
await textClient.connect();
|
|
169
169
|
textClient.sendText('What is the weather like?');
|
|
@@ -199,7 +199,7 @@ arrives), `"long"` for agent or batch jobs; leave it out and Dialt learns from o
|
|
|
199
199
|
|
|
200
200
|
```js
|
|
201
201
|
mode: {
|
|
202
|
-
kind: "
|
|
202
|
+
kind: "dialt",
|
|
203
203
|
tools: [{
|
|
204
204
|
name: "lookup_order",
|
|
205
205
|
description: "Look up an order by ID.",
|
|
@@ -216,7 +216,7 @@ anything the caller should not wait on — declare the tool as a background job:
|
|
|
216
216
|
|
|
217
217
|
```js
|
|
218
218
|
mode: {
|
|
219
|
-
kind: "
|
|
219
|
+
kind: "dialt",
|
|
220
220
|
tools: [{
|
|
221
221
|
name: "run_task",
|
|
222
222
|
description: "Run a long coding task and report when done.",
|
|
@@ -271,7 +271,7 @@ an explicit user request to cancel pending work; hosts should stop that call pro
|
|
|
271
271
|
Experimental: the API is stable, but this transport is newly shipped and still being hardened on
|
|
272
272
|
real networks; `ws` remains the default and recommended fallback.
|
|
273
273
|
|
|
274
|
-
Pass `transport: 'webrtc'` to `
|
|
274
|
+
Pass `transport: 'webrtc'` to `DialtClient` to carry the call over WebRTC (UDP) instead of the
|
|
275
275
|
default WebSocket — more resilient to jitter and packet loss on weak networks. `ws` remains the
|
|
276
276
|
default. Safari/WebKit falls back to `ws` automatically. See the
|
|
277
277
|
[browser guide's WebRTC section](https://dialt.com/docs/api/browser/#webrtc).
|
|
@@ -284,7 +284,7 @@ Dialt service, its models, or its server-side implementation.
|
|
|
284
284
|
### Ambience: background music and the thinking sound
|
|
285
285
|
|
|
286
286
|
```js
|
|
287
|
-
const client = new
|
|
287
|
+
const client = new DialtClient({ url, sessionId, apiKey, mode, ambience: 'thinking' });
|
|
288
288
|
client.setAmbience('continuous'); // switch live; 'off' | 'thinking' | 'continuous'
|
|
289
289
|
```
|
|
290
290
|
|
package/package.json
CHANGED
package/src/ambience.js
CHANGED
|
@@ -385,7 +385,7 @@ export class AmbienceBed {
|
|
|
385
385
|
// --- controller ------------------------------------------------------------------------------
|
|
386
386
|
|
|
387
387
|
/** Drives one AmbienceBed on the client's player from the session's events. Owned by
|
|
388
|
-
*
|
|
388
|
+
* DialtClient (its `ambience` option); apps interact through `client.setAmbience(mode)`.
|
|
389
389
|
*
|
|
390
390
|
* Holds facts only - mode, whether a reply has played this session, whether Dialt is working
|
|
391
391
|
* and whether the thinking threshold has elapsed - and derives the bed's envelope target from
|
package/src/index.js
CHANGED
|
@@ -95,7 +95,7 @@ function sendOneShotFrame(kind, frame, { url, WebSocketImpl = globalThis.WebSock
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
// Post-session feedback (thumbs + optional comment + device/browser tags). The server files it
|
|
98
|
-
// next to the session's recording, so pass the
|
|
98
|
+
// next to the session's recording, so pass the DialtClient's sessionId.
|
|
99
99
|
export function sendFeedback({ url, sessionId, rating, text, device, browser, apiKey,
|
|
100
100
|
WebSocketImpl, timeoutMs } = {}) {
|
|
101
101
|
if (!url || !sessionId) return Promise.reject(new Error('url and sessionId are required'));
|
|
@@ -125,7 +125,7 @@ export function sendClientError({ url, sessionId, detail, context, apiKey,
|
|
|
125
125
|
return sendOneShotFrame('client_error', frame, { url, WebSocketImpl, timeoutMs });
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
const
|
|
128
|
+
const DIALT_MODE_FIELDS = new Set([
|
|
129
129
|
'kind', 'modality', 'voice', 'instructions', 'tools', 'web_search', 'end_call', 'flow', 'greeting',
|
|
130
130
|
'temperature', 'silence_nudge_s', 'silence_end_s', 'tool_choice', 'background_audio',
|
|
131
131
|
]);
|
|
@@ -147,17 +147,18 @@ function validatedToolChoice(value) {
|
|
|
147
147
|
}
|
|
148
148
|
const RELAY_MODE_FIELDS = new Set(['kind', 'provider', 'model', 'voice', 'web_search']);
|
|
149
149
|
|
|
150
|
-
function validatedMode(value = { kind: '
|
|
150
|
+
function validatedMode(value = { kind: 'dialt' }) {
|
|
151
151
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
152
152
|
throw new TypeError('mode must be an object');
|
|
153
153
|
}
|
|
154
154
|
const mode = { ...value };
|
|
155
|
+
if (mode.kind === 'converse') mode.kind = 'dialt';
|
|
155
156
|
for (const [key, item] of Object.entries(mode)) {
|
|
156
157
|
if (item === undefined) delete mode[key];
|
|
157
158
|
}
|
|
158
|
-
const allowed = mode.kind === '
|
|
159
|
+
const allowed = mode.kind === 'dialt' ? DIALT_MODE_FIELDS
|
|
159
160
|
: mode.kind === 'relay' ? RELAY_MODE_FIELDS : null;
|
|
160
|
-
if (!allowed) throw new TypeError('mode.kind must be
|
|
161
|
+
if (!allowed) throw new TypeError('mode.kind must be dialt or relay');
|
|
161
162
|
const extra = Object.keys(mode).find((key) => !allowed.has(key));
|
|
162
163
|
if (extra) throw new TypeError(`unexpected ${mode.kind} mode field: ${extra}`);
|
|
163
164
|
const optionalString = (key) => {
|
|
@@ -173,9 +174,9 @@ function validatedMode(value = { kind: 'converse' }) {
|
|
|
173
174
|
optionalString('voice');
|
|
174
175
|
optionalBoolean('web_search');
|
|
175
176
|
mode.web_search ??= false;
|
|
176
|
-
if (mode.kind === '
|
|
177
|
+
if (mode.kind === 'dialt') {
|
|
177
178
|
if (mode.modality !== undefined && mode.modality !== 'voice' && mode.modality !== 'text') {
|
|
178
|
-
throw new TypeError('
|
|
179
|
+
throw new TypeError('dialt modality must be voice or text');
|
|
179
180
|
}
|
|
180
181
|
mode.modality ??= 'voice';
|
|
181
182
|
optionalString('instructions');
|
|
@@ -185,20 +186,20 @@ function validatedMode(value = { kind: 'converse' }) {
|
|
|
185
186
|
optionalBoolean('flow');
|
|
186
187
|
optionalBoolean('background_audio');
|
|
187
188
|
if (Object.hasOwn(mode, 'tools') && !Array.isArray(mode.tools)) {
|
|
188
|
-
throw new TypeError('
|
|
189
|
+
throw new TypeError('dialt tools must be an array');
|
|
189
190
|
}
|
|
190
191
|
if (Object.hasOwn(mode, 'tool_choice')) {
|
|
191
192
|
if (!Array.isArray(mode.tools) || !mode.tools.length) {
|
|
192
|
-
throw new TypeError('
|
|
193
|
+
throw new TypeError('dialt tool_choice requires a non-empty tools list');
|
|
193
194
|
}
|
|
194
195
|
mode.tool_choice = validatedToolChoice(mode.tool_choice);
|
|
195
196
|
}
|
|
196
197
|
if (mode.greeting != null && mode.greeting !== false && typeof mode.greeting !== 'string') {
|
|
197
|
-
throw new TypeError('
|
|
198
|
+
throw new TypeError('dialt greeting must be a string or false');
|
|
198
199
|
}
|
|
199
200
|
if (Object.hasOwn(mode, 'temperature') && (typeof mode.temperature !== 'number'
|
|
200
201
|
|| !Number.isFinite(mode.temperature))) {
|
|
201
|
-
throw new TypeError('
|
|
202
|
+
throw new TypeError('dialt temperature must be a finite number');
|
|
202
203
|
}
|
|
203
204
|
// Per-session override of the broker's two-stage silence policy (env defaults: 10s/20s) — e.g.
|
|
204
205
|
// a benchmark harness with long simulated-user think-time. Omit either field to keep the
|
|
@@ -207,12 +208,12 @@ function validatedMode(value = { kind: 'converse' }) {
|
|
|
207
208
|
for (const key of ['silence_nudge_s', 'silence_end_s']) {
|
|
208
209
|
if (Object.hasOwn(mode, key) && (typeof mode[key] !== 'number'
|
|
209
210
|
|| !Number.isFinite(mode[key]) || mode[key] <= 0)) {
|
|
210
|
-
throw new TypeError(`
|
|
211
|
+
throw new TypeError(`dialt ${key} must be a positive finite number`);
|
|
211
212
|
}
|
|
212
213
|
}
|
|
213
214
|
if (Object.hasOwn(mode, 'silence_nudge_s') && Object.hasOwn(mode, 'silence_end_s')
|
|
214
215
|
&& mode.silence_end_s <= mode.silence_nudge_s) {
|
|
215
|
-
throw new TypeError('
|
|
216
|
+
throw new TypeError('dialt silence_end_s must be greater than silence_nudge_s');
|
|
216
217
|
}
|
|
217
218
|
} else {
|
|
218
219
|
if (typeof mode.provider !== 'string' || !mode.provider.trim()) {
|
|
@@ -246,9 +247,9 @@ function connectMarks() {
|
|
|
246
247
|
return { marks, mark };
|
|
247
248
|
}
|
|
248
249
|
|
|
249
|
-
export class
|
|
250
|
+
export class DialtClient extends EventTarget {
|
|
250
251
|
constructor({ url, sessionId = createSessionId(), player, apiKey,
|
|
251
|
-
mode = { kind: '
|
|
252
|
+
mode = { kind: 'dialt' }, user, timezone, rawAssist = false,
|
|
252
253
|
WebSocketImpl = globalThis.WebSocket,
|
|
253
254
|
echoCancellerFactory = () => new EchoCanceller(),
|
|
254
255
|
autoReconnect = true, reconnectBaseMs = 500, reconnectMaxMs = 5000,
|
|
@@ -277,7 +278,7 @@ export class ConverseClient extends EventTarget {
|
|
|
277
278
|
// the engine, not the browser app, since iOS forces every browser onto WebKit. The track
|
|
278
279
|
// arrives; Web Audio gets silence. Anyone revisiting this should re-run that probe before
|
|
279
280
|
// writing code, not after.
|
|
280
|
-
console.warn('[
|
|
281
|
+
console.warn('[dialt] webrtc transport is not yet supported on WebKit - using ws');
|
|
281
282
|
transport = 'ws';
|
|
282
283
|
}
|
|
283
284
|
this.url = toWebSocketUrl(url);
|
|
@@ -298,7 +299,7 @@ export class ConverseClient extends EventTarget {
|
|
|
298
299
|
ambienceOpts.mode ??= 'thinking';
|
|
299
300
|
const ambienceOnWs = transport === 'ws';
|
|
300
301
|
if (!ambienceOnWs && ambienceGiven && ambienceOpts.mode !== 'off') {
|
|
301
|
-
console.warn('[
|
|
302
|
+
console.warn('[dialt] ambience is not available on the webrtc transport (use mode.background_audio); disabled');
|
|
302
303
|
}
|
|
303
304
|
this.ambience = new Ambience({ player: this.player, enabled: ambienceOnWs, ...ambienceOpts });
|
|
304
305
|
this.apiKey = apiKey || null;
|
|
@@ -823,7 +824,7 @@ export class ConverseClient extends EventTarget {
|
|
|
823
824
|
// Shared start-frame construction (mode/temperature/greeting/rawAssist) for both transports.
|
|
824
825
|
_buildStartFrame() {
|
|
825
826
|
let mode = validatedMode(this._mode);
|
|
826
|
-
if (mode.kind === '
|
|
827
|
+
if (mode.kind === 'dialt') {
|
|
827
828
|
if (this._temperature != null) mode.temperature = this._temperature;
|
|
828
829
|
if (this._noGreeting) mode.greeting = false;
|
|
829
830
|
if (mode.background_audio && this.transport !== 'webrtc') {
|
|
@@ -833,15 +834,15 @@ export class ConverseClient extends EventTarget {
|
|
|
833
834
|
// been downgraded for us: WebKit silently falls back to ws (see the constructor), so a
|
|
834
835
|
// caller who asked for webrtc + background_audio would otherwise lose the whole session
|
|
835
836
|
// on iOS instead of just the music.
|
|
836
|
-
console.warn('[
|
|
837
|
+
console.warn('[dialt] background_audio requires the webrtc transport - disabled');
|
|
837
838
|
delete mode.background_audio;
|
|
838
839
|
}
|
|
839
840
|
}
|
|
840
841
|
mode = validatedMode(mode);
|
|
841
|
-
const textModality = mode.kind === '
|
|
842
|
+
const textModality = mode.kind === 'dialt' && mode.modality === 'text';
|
|
842
843
|
// Voice is the established wire default. Keep it internally for client-side guards, but omit
|
|
843
844
|
// it from the frame so this SDK remains voice-compatible with pre-text Dialt servers.
|
|
844
|
-
if (mode.kind === '
|
|
845
|
+
if (mode.kind === 'dialt' && mode.modality === 'voice') delete mode.modality;
|
|
845
846
|
const start = {
|
|
846
847
|
type: 'start',
|
|
847
848
|
session_id: this.sessionId,
|
|
@@ -850,7 +851,7 @@ export class ConverseClient extends EventTarget {
|
|
|
850
851
|
if (!textModality) {
|
|
851
852
|
start.audio = { sr: SAMPLE_RATE, output_encoding: 'pcm16' };
|
|
852
853
|
} else if (this.transport !== 'ws') {
|
|
853
|
-
throw new TypeError('
|
|
854
|
+
throw new TypeError('dialt text modality requires the ws transport');
|
|
854
855
|
}
|
|
855
856
|
if (this.apiKey) start.api_key = this.apiKey;
|
|
856
857
|
if (this._resumeToken) start.resume_token = this._resumeToken;
|
|
@@ -1330,7 +1331,7 @@ export class ConverseClient extends EventTarget {
|
|
|
1330
1331
|
try { fn(); } catch (err) {
|
|
1331
1332
|
if (!this._ambienceWarned) {
|
|
1332
1333
|
this._ambienceWarned = true;
|
|
1333
|
-
console.warn('[
|
|
1334
|
+
console.warn('[dialt] ambience error (disabled for this session):', err);
|
|
1334
1335
|
}
|
|
1335
1336
|
try { this.ambience.setMode('off'); } catch { /* already off */ }
|
|
1336
1337
|
}
|
|
@@ -1563,7 +1564,7 @@ export class ConverseClient extends EventTarget {
|
|
|
1563
1564
|
* changes nothing; a mid-session `setTools` resets tool_choice to `"auto"`. */
|
|
1564
1565
|
setToolChoice(toolChoice, { oneShot = false } = {}) {
|
|
1565
1566
|
const validated = validatedToolChoice(toolChoice);
|
|
1566
|
-
if (!oneShot && this._mode.kind === '
|
|
1567
|
+
if (!oneShot && this._mode.kind === 'dialt'
|
|
1567
1568
|
&& Array.isArray(this._mode.tools) && this._mode.tools.length) {
|
|
1568
1569
|
// Durable restrictions fold into the replayed mode (like setVoice) so an auto-reconnect
|
|
1569
1570
|
// re-applies them; a one-shot is turn-scoped and deliberately does not survive. A mode
|
|
@@ -1580,7 +1581,7 @@ export class ConverseClient extends EventTarget {
|
|
|
1580
1581
|
setVoice(voice) {
|
|
1581
1582
|
// Relay providers bind their voice when the upstream session is constructed and do not support
|
|
1582
1583
|
// this control. Dialt reconnects replay the selected voice.
|
|
1583
|
-
if (this._mode.kind !== '
|
|
1584
|
+
if (this._mode.kind !== 'dialt') return;
|
|
1584
1585
|
this._mode = Object.freeze(validatedMode({ ...this._mode, voice }));
|
|
1585
1586
|
this._sendControl({ type: 'set_voice', voice });
|
|
1586
1587
|
}
|
|
@@ -1796,3 +1797,6 @@ export class ConverseClient extends EventTarget {
|
|
|
1796
1797
|
return detail;
|
|
1797
1798
|
}
|
|
1798
1799
|
}
|
|
1800
|
+
|
|
1801
|
+
/** @deprecated Use DialtClient. */
|
|
1802
|
+
export const ConverseClient = DialtClient;
|
package/src/mic.js
CHANGED
|
@@ -25,7 +25,7 @@ export class CaptureAbortedError extends Error {
|
|
|
25
25
|
// distorts levels — so an app that hand-rolls getUserMedia({audio:true}) silently degrades the
|
|
26
26
|
// loop. Frames come out as 16 kHz 512-sample Float32 via an AudioWorklet resampler.
|
|
27
27
|
// `processing:false` opens the mic fully raw (AEC+NS+AGC off): used for the optional raw ablation
|
|
28
|
-
// track, and by
|
|
28
|
+
// track, and by DialtClient.startMic on WebKit where the SDK's own AEC3 cancels instead.
|
|
29
29
|
export class MicCapture {
|
|
30
30
|
constructor({ onFrame, processing = true, workletUrl, deviceId } = {}) {
|
|
31
31
|
this.onFrame = onFrame;
|
package/src/webrtc.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
// WebRTC transport support for
|
|
1
|
+
// WebRTC transport support for DialtClient - see serving/broker_webrtc.py's module docstring
|
|
2
2
|
// for the full wire contract this mirrors. Two independent pieces live here:
|
|
3
3
|
//
|
|
4
4
|
// TrackFeeder — turns mic frames (whatever startMic()/pushMicFrame() would otherwise ws.send()
|
|
5
5
|
// as PCM16) into a real outbound MediaStreamTrack, by re-injecting them through an
|
|
6
6
|
// AudioWorkletNode into a MediaStreamAudioDestinationNode. RTCPeerConnection.
|
|
7
7
|
// addTrack() needs a live MediaStreamTrack before the offer/ICE-gather/answer
|
|
8
|
-
// exchange even happens
|
|
8
|
+
// exchange even happens - and that exchange has to complete before DialtClient
|
|
9
9
|
// knows whether the app will call startMic() at all (connect() resolves first) —
|
|
10
10
|
// so this feeder's track is always what negotiates the offer's audio m-line.
|
|
11
|
-
// Once startMic() runs (the normal path: no forced SDK-side AEC),
|
|
11
|
+
// Once startMic() runs (the normal path: no forced SDK-side AEC), DialtClient
|
|
12
12
|
// replaceTrack()s the sender straight onto the real getUserMedia device track —
|
|
13
13
|
// zero added JS hops/latency, no renegotiation needed — and this feeder goes idle.
|
|
14
14
|
// It stays the ACTIVE uplink only for callers with no MicCapture at all (custom
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
// round-trip, acceptable latency for a single-hop broker-terminated call), and
|
|
24
24
|
// later applies the server's answer.
|
|
25
25
|
//
|
|
26
|
-
// Both are deliberately dumb/mockable:
|
|
26
|
+
// Both are deliberately dumb/mockable: DialtClient owns all protocol semantics (start frame,
|
|
27
27
|
// ready/bye handling, reconnection policy); this module only knows WebRTC plumbing.
|
|
28
28
|
|
|
29
29
|
import { addWorkletModule, defaultWorkletModuleUrls } from './worklet-url.js';
|
|
@@ -157,7 +157,7 @@ export class WebRtcSession {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
/** Returns the RTCRtpSender so the caller can later replaceTrack() a real capture device's
|
|
160
|
-
* track in directly (see
|
|
160
|
+
* track in directly (see DialtClient.startMic) without renegotiating. */
|
|
161
161
|
addAudioTrack(track) {
|
|
162
162
|
return this.pc.addTrack(track);
|
|
163
163
|
}
|