@lumavox/client 0.1.1 → 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/README.md +8 -5
- package/dist/client.d.ts +36 -23
- package/dist/client.js +35 -25
- package/dist/conversation.d.ts +3 -2
- package/dist/conversation.js +18 -16
- package/dist/errors.d.ts +24 -16
- package/dist/errors.js +19 -39
- package/dist/events.d.ts +25 -20
- package/dist/gen/index.d.ts +1 -1
- package/dist/gen/types.gen.d.ts +203 -260
- package/dist/protocol.gen.d.ts +4 -4
- package/dist/protocol.gen.js +4 -4
- package/dist/token.d.ts +1 -1
- package/dist/transport.js +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ const conversation = lumavox.connect(session.id, {
|
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
const outcome = await conversation.send("What should I see on my first morning?");
|
|
29
|
-
// { status: "done", frame } | { status: "interrupted", frame } | { status: "dropped", reason }
|
|
29
|
+
// { status: "done", frame } | { status: "interrupted", frame } | { status: "dropped", reason, message }
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
React and React Native: `@lumavox/react`, hooks over this.
|
|
@@ -55,15 +55,18 @@ for a 402, 403 or 429. `clearToken()` at sign-out.
|
|
|
55
55
|
| Resource | Methods |
|
|
56
56
|
|---|---|
|
|
57
57
|
| `sessions` | `create`, `list` (every one), `page`, `get`, `history` (the whole transcript, newest first), `delete`, `compact`, `extract` |
|
|
58
|
-
| `facts` | `list`, `delete`, `deleteAll` |
|
|
59
|
-
| `relations` | `list` |
|
|
58
|
+
| `facts` | `list`, `page`, `delete`, `deleteAll` |
|
|
59
|
+
| `relations` | `list`, `page` |
|
|
60
60
|
|
|
61
61
|
Bodies and answers are the API's own JSON, as the API reference shows them;
|
|
62
62
|
the SDK's options go in a last argument: `{ timeoutMs, signal, maxRetries,
|
|
63
|
-
requestId }`.
|
|
63
|
+
requestId }`. `list()` is every item, page after page by each page's
|
|
64
|
+
`next_cursor`; `page()` is one page, `{ data, next_cursor }`; a session's
|
|
65
|
+
messages are a page inside it, read back by `messages.next_cursor`. A
|
|
66
|
+
delete answers `true`, and `false` for what is already gone. Errors are
|
|
64
67
|
`PlatformError` and its subclasses — `AuthenticationError`,
|
|
65
68
|
`PermissionError`, `NotFoundError`, `RateLimitError`, … — with `status`,
|
|
66
|
-
`
|
|
69
|
+
`code`, `requestId`, `issues` and `retryAfter`, as `@lumavox/server`'s.
|
|
67
70
|
|
|
68
71
|
## A conversation
|
|
69
72
|
|
package/dist/client.d.ts
CHANGED
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* - **Shaped as the server SDK is** (`sdks/server`): bodies and answers in
|
|
8
8
|
* the wire's snake_case, camelCase methods, the SDK's options
|
|
9
9
|
* (`timeoutMs`, `signal`, `maxRetries`, `requestId`) last; `list()` every
|
|
10
|
-
* item,
|
|
11
|
-
* `
|
|
10
|
+
* item, page after page by each page's `next_cursor`, and `page()` one
|
|
11
|
+
* page, `{ data, next_cursor }`; a delete answers `true`, and `false`
|
|
12
|
+
* for what is already gone; the same `transport.ts` and `errors.ts`,
|
|
13
|
+
* copied.
|
|
12
14
|
* - **A key is asked for once, and replaced once**: a call refused with 401
|
|
13
15
|
* asks for a new key and is sent again; a second refusal, on the fresh
|
|
14
16
|
* key, is the caller's (`token.ts`). **Never on 402, 403 or 429**: on this
|
|
@@ -20,7 +22,7 @@
|
|
|
20
22
|
* and later, browsers, React Native.
|
|
21
23
|
*/
|
|
22
24
|
import { type ConnectOptions, Conversation, type ConversationHandlers } from "./conversation.js";
|
|
23
|
-
import type { CompactionInfo, CompactRequest,
|
|
25
|
+
import type { CompactionInfo, CompactRequest, FactInfo, FactPage, FactsDeleted, MessageInfo, RelationInfo, RelationPage, SessionCreateRequest, SessionDetail, SessionInfo, SessionPage } from "./gen/index.js";
|
|
24
26
|
import type { WebSocketConstructor } from "./socket.js";
|
|
25
27
|
import { type GetToken } from "./token.js";
|
|
26
28
|
import { type Fetch, type RequestOptions } from "./transport.js";
|
|
@@ -48,49 +50,60 @@ export interface ClientOptions {
|
|
|
48
50
|
export interface SessionListParams {
|
|
49
51
|
/** How many a page holds, up to 200. */
|
|
50
52
|
limit?: number;
|
|
51
|
-
|
|
53
|
+
/** `page()` only: the `next_cursor` of the page before. */
|
|
54
|
+
cursor?: string;
|
|
52
55
|
}
|
|
53
56
|
export interface SessionGetParams {
|
|
54
57
|
/** Messages, counting back from the newest: 50 by default, up to 200. */
|
|
55
58
|
limit?: number;
|
|
56
|
-
/**
|
|
57
|
-
|
|
59
|
+
/** The older messages: `messages.next_cursor` of the page before. */
|
|
60
|
+
cursor?: string;
|
|
58
61
|
}
|
|
59
62
|
export interface Sessions {
|
|
60
63
|
/** Open a conversation with an agent — or resume the person's last, by default (`resume`). */
|
|
61
64
|
create(body: SessionCreateRequest, options?: RequestOptions): Promise<SessionDetail>;
|
|
62
|
-
/** Every one of the person's conversations,
|
|
63
|
-
list(params?: SessionListParams, options?: RequestOptions): AsyncGenerator<SessionInfo, void, undefined>;
|
|
64
|
-
/** One page of them. */
|
|
65
|
-
page(params?: SessionListParams, options?: RequestOptions): Promise<
|
|
66
|
-
/** One conversation, with a page of its transcript. */
|
|
65
|
+
/** Every one of the person's conversations, most recent first, page after page. */
|
|
66
|
+
list(params?: Omit<SessionListParams, "cursor">, options?: RequestOptions): AsyncGenerator<SessionInfo, void, undefined>;
|
|
67
|
+
/** One page of them, with the `next_cursor` of the next. */
|
|
68
|
+
page(params?: SessionListParams, options?: RequestOptions): Promise<SessionPage>;
|
|
69
|
+
/** One conversation, with a page of its transcript: the newest messages, oldest first. */
|
|
67
70
|
get(id: string, params?: SessionGetParams, options?: RequestOptions): Promise<SessionDetail>;
|
|
68
|
-
/** Its whole transcript, newest message first, paging back by `
|
|
71
|
+
/** Its whole transcript, newest message first, paging back by `messages.next_cursor`. */
|
|
69
72
|
history(id: string, options?: RequestOptions): AsyncGenerator<MessageInfo, void, undefined>;
|
|
70
|
-
/** Delete it
|
|
71
|
-
delete(id: string, options?: RequestOptions): Promise<
|
|
73
|
+
/** Delete it: `true`, or `false` when it is already gone. */
|
|
74
|
+
delete(id: string, options?: RequestOptions): Promise<boolean>;
|
|
72
75
|
/** Summarise it now; `extract_facts` also stores what is worth remembering. */
|
|
73
76
|
compact(id: string, body?: CompactRequest, options?: RequestOptions): Promise<CompactionInfo>;
|
|
74
77
|
/** Compact it and extract facts, in one model call. */
|
|
75
78
|
extract(id: string, options?: RequestOptions): Promise<CompactionInfo>;
|
|
76
79
|
}
|
|
77
80
|
export interface Facts {
|
|
78
|
-
/**
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Everything remembered about the person, oldest first — the newest 100
|
|
83
|
+
* for now, in one page (left-overs #109); read on by cursor the day the
|
|
84
|
+
* route pages.
|
|
85
|
+
*/
|
|
86
|
+
list(options?: RequestOptions): AsyncGenerator<FactInfo, void, undefined>;
|
|
87
|
+
/** One page of them, as the route answers it. */
|
|
88
|
+
page(options?: RequestOptions): Promise<FactPage>;
|
|
89
|
+
/** Forget one: `true`, or `false` when it is already gone. */
|
|
90
|
+
delete(id: string, options?: RequestOptions): Promise<boolean>;
|
|
91
|
+
/** Forget everything about the person: how many facts went. Their conversations stay. */
|
|
92
|
+
deleteAll(options?: RequestOptions): Promise<FactsDeleted>;
|
|
84
93
|
}
|
|
85
94
|
export interface RelationListParams {
|
|
86
95
|
/** One agent's row alone. */
|
|
87
96
|
agent_id?: string;
|
|
88
|
-
/**
|
|
97
|
+
/** How many a page holds: 50 by default, up to 200. */
|
|
89
98
|
limit?: number;
|
|
99
|
+
/** `page()` only: the `next_cursor` of the page before. */
|
|
100
|
+
cursor?: string;
|
|
90
101
|
}
|
|
91
102
|
export interface Relations {
|
|
92
|
-
/** How the agents the person talked to feel about them, most
|
|
93
|
-
list(params?: RelationListParams, options?: RequestOptions):
|
|
103
|
+
/** How the agents the person talked to feel about them, most recently active first. */
|
|
104
|
+
list(params?: Omit<RelationListParams, "cursor">, options?: RequestOptions): AsyncGenerator<RelationInfo, void, undefined>;
|
|
105
|
+
/** One page of them, with the `next_cursor` of the next. */
|
|
106
|
+
page(params?: RelationListParams, options?: RequestOptions): Promise<RelationPage>;
|
|
94
107
|
}
|
|
95
108
|
export interface Client {
|
|
96
109
|
readonly sessions: Sessions;
|
package/dist/client.js
CHANGED
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* - **Shaped as the server SDK is** (`sdks/server`): bodies and answers in
|
|
8
8
|
* the wire's snake_case, camelCase methods, the SDK's options
|
|
9
9
|
* (`timeoutMs`, `signal`, `maxRetries`, `requestId`) last; `list()` every
|
|
10
|
-
* item,
|
|
11
|
-
* `
|
|
10
|
+
* item, page after page by each page's `next_cursor`, and `page()` one
|
|
11
|
+
* page, `{ data, next_cursor }`; a delete answers `true`, and `false`
|
|
12
|
+
* for what is already gone; the same `transport.ts` and `errors.ts`,
|
|
13
|
+
* copied.
|
|
12
14
|
* - **A key is asked for once, and replaced once**: a call refused with 401
|
|
13
15
|
* asks for a new key and is sent again; a second refusal, on the fresh
|
|
14
16
|
* key, is the caller's (`token.ts`). **Never on 402, 403 or 429**: on this
|
|
@@ -26,6 +28,17 @@ import { orNull, segment, Transport, } from "./transport.js";
|
|
|
26
28
|
export const DEFAULT_BASE_URL = "https://api.lumavox.ai";
|
|
27
29
|
/** A page as large as the route allows: fewer calls for a long list. */
|
|
28
30
|
const PAGE = 200;
|
|
31
|
+
/** Every item, page after page by cursor, until a page names no next one. */
|
|
32
|
+
async function* pages(read) {
|
|
33
|
+
let cursor;
|
|
34
|
+
do {
|
|
35
|
+
const page = await read(cursor);
|
|
36
|
+
yield* page.data;
|
|
37
|
+
cursor = page.next_cursor ?? undefined;
|
|
38
|
+
} while (cursor);
|
|
39
|
+
}
|
|
40
|
+
/** `true` once `promise` is done, `false` when it was a 404: what is not there is gone. */
|
|
41
|
+
const done = (promise) => orNull(promise).then((answer) => answer !== null);
|
|
29
42
|
export function createClient(options) {
|
|
30
43
|
if (typeof options?.getToken !== "function") {
|
|
31
44
|
throw new TypeError("createClient needs getToken: a function answering the end user's ephemeral key.");
|
|
@@ -53,53 +66,50 @@ export function createClient(options) {
|
|
|
53
66
|
throw error;
|
|
54
67
|
const fresh = await tokens.replace(key, {
|
|
55
68
|
status: 401,
|
|
56
|
-
code: error.
|
|
69
|
+
code: error.code,
|
|
57
70
|
});
|
|
58
71
|
return (await transport.request({ ...call, token: fresh }, request))
|
|
59
72
|
.data;
|
|
60
73
|
}
|
|
61
74
|
}
|
|
62
75
|
const sessionPath = (id, rest = "") => `/v1/agent/sessions/${segment(id)}${rest}`;
|
|
63
|
-
|
|
76
|
+
/** One page of the list at `path`. */
|
|
77
|
+
const read = (path, params = {}, request) => send({ method: "GET", path, query: { ...params } }, request);
|
|
78
|
+
const page = (params = {}, request) => read("/v1/agent/sessions", params, request);
|
|
79
|
+
const factPage = (request, cursor) => read("/v1/agent/facts", { cursor }, request);
|
|
80
|
+
const relationPage = (params = {}, request) => read("/v1/agent/relations", params, request);
|
|
64
81
|
const get = (id, params = {}, request) => send({ method: "GET", path: sessionPath(id), query: { ...params } }, request);
|
|
65
82
|
return {
|
|
66
83
|
sessions: {
|
|
67
84
|
create: (body, request) => send({ method: "POST", path: "/v1/agent/sessions", body }, request),
|
|
68
|
-
|
|
69
|
-
const limit = params.limit ?? PAGE;
|
|
70
|
-
let offset = params.offset ?? 0;
|
|
71
|
-
for (;;) {
|
|
72
|
-
const items = await page({ limit, offset }, request);
|
|
73
|
-
yield* items;
|
|
74
|
-
offset += items.length;
|
|
75
|
-
if (items.length < limit)
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
},
|
|
85
|
+
list: (params = {}, request) => pages((cursor) => page({ limit: PAGE, ...params, cursor }, request)),
|
|
79
86
|
page,
|
|
80
87
|
get,
|
|
81
88
|
async *history(id, request) {
|
|
82
|
-
let
|
|
89
|
+
let cursor;
|
|
83
90
|
do {
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
91
|
+
const { messages } = await get(id, { limit: PAGE, cursor: cursor ?? undefined }, request);
|
|
92
|
+
// Each page is oldest first; the history runs newest first.
|
|
93
|
+
for (let i = messages.data.length - 1; i >= 0; i--) {
|
|
94
|
+
yield messages.data[i];
|
|
87
95
|
}
|
|
88
|
-
|
|
89
|
-
} while (
|
|
96
|
+
cursor = messages.next_cursor;
|
|
97
|
+
} while (cursor);
|
|
90
98
|
},
|
|
91
|
-
delete: (id, request) =>
|
|
99
|
+
delete: (id, request) => done(send({ method: "DELETE", path: sessionPath(id) }, request)),
|
|
92
100
|
// The document requires a body; none is `{}`, the agent's own setting.
|
|
93
101
|
compact: (id, body = {}, request) => send({ method: "POST", path: sessionPath(id, "/compact"), body }, request),
|
|
94
102
|
extract: (id, request) => send({ method: "POST", path: sessionPath(id, "/extract") }, request),
|
|
95
103
|
},
|
|
96
104
|
facts: {
|
|
97
|
-
list: (request) =>
|
|
98
|
-
|
|
105
|
+
list: (request) => pages((cursor) => factPage(request, cursor)),
|
|
106
|
+
page: factPage,
|
|
107
|
+
delete: (id, request) => done(send({ method: "DELETE", path: `/v1/agent/facts/${segment(id)}` }, request)),
|
|
99
108
|
deleteAll: (request) => send({ method: "DELETE", path: "/v1/agent/facts" }, request),
|
|
100
109
|
},
|
|
101
110
|
relations: {
|
|
102
|
-
list: (params = {}, request) =>
|
|
111
|
+
list: (params = {}, request) => pages((cursor) => relationPage({ limit: PAGE, ...params, cursor }, request)),
|
|
112
|
+
page: relationPage,
|
|
103
113
|
},
|
|
104
114
|
connect(sessionId, handlers = {}, connectOptions = {}) {
|
|
105
115
|
const WebSocket = options.WebSocket ??
|
package/dist/conversation.d.ts
CHANGED
|
@@ -96,7 +96,7 @@ export type TurnOutcome = {
|
|
|
96
96
|
export type TranscriptOutcome = {
|
|
97
97
|
status: "done";
|
|
98
98
|
text: string;
|
|
99
|
-
|
|
99
|
+
audioSeconds: number;
|
|
100
100
|
frame: TranscriptFrame;
|
|
101
101
|
}
|
|
102
102
|
/** The edge refused or failed it: a rate it does not take, no audio, the provider. */
|
|
@@ -108,7 +108,8 @@ export type TranscriptOutcome = {
|
|
|
108
108
|
export interface Dropped {
|
|
109
109
|
status: "dropped";
|
|
110
110
|
reason: "closed" | "cancelled" | "refused";
|
|
111
|
-
|
|
111
|
+
/** The edge's words for a refusal; `null` otherwise. */
|
|
112
|
+
message: string | null;
|
|
112
113
|
}
|
|
113
114
|
/** Why a conversation ended without reconnecting. */
|
|
114
115
|
export interface CloseInfo {
|
package/dist/conversation.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { wireBytes } from "./pcm.js";
|
|
2
|
-
import {
|
|
2
|
+
import { KEEPALIVE_SECONDS, MAX_AUDIO_BYTES, MAX_MESSAGE_CHARS, RECORDING_RATES, } from "./protocol.gen.js";
|
|
3
3
|
import { dial, isFinalClose, reconnectAfter, socketUrl, } from "./socket.js";
|
|
4
4
|
/** No `ready` this long after the dial — a proxy that took the upgrade and says nothing — is a dropped connection. */
|
|
5
5
|
export const READY_DEADLINE_MS = 15000;
|
|
@@ -52,9 +52,11 @@ function call(fn, value) {
|
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
-
const dropped = (reason,
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
const dropped = (reason, message) => ({
|
|
56
|
+
status: "dropped",
|
|
57
|
+
reason,
|
|
58
|
+
message: message ?? null,
|
|
59
|
+
});
|
|
58
60
|
/** Characters as the edge counts them: code points. */
|
|
59
61
|
function characters(text) {
|
|
60
62
|
let n = 0;
|
|
@@ -164,7 +166,7 @@ export class Conversation {
|
|
|
164
166
|
recording.dropped = "closed";
|
|
165
167
|
return id;
|
|
166
168
|
}
|
|
167
|
-
const frame = this.turnFrame({ type: "user_audio.start", id, rate }, options);
|
|
169
|
+
const frame = this.turnFrame({ type: "user_audio.start", recording_id: id, rate }, options);
|
|
168
170
|
if (language)
|
|
169
171
|
frame.language = language;
|
|
170
172
|
if (model)
|
|
@@ -217,7 +219,7 @@ export class Conversation {
|
|
|
217
219
|
if (failed) {
|
|
218
220
|
return Promise.resolve(options.transcribeOnly
|
|
219
221
|
? { status: "failed", frame: failed }
|
|
220
|
-
: dropped("refused", failed.
|
|
222
|
+
: dropped("refused", failed.message));
|
|
221
223
|
}
|
|
222
224
|
if (recording.dropped || this.closed) {
|
|
223
225
|
return Promise.resolve(dropped(recording.dropped ?? "closed"));
|
|
@@ -327,10 +329,10 @@ export class Conversation {
|
|
|
327
329
|
this.deadline = setTimeout(() => this.drop(generation, "The connection never became ready."), READY_DEADLINE_MS);
|
|
328
330
|
this.watchdog = setInterval(() => {
|
|
329
331
|
const quiet = Date.now() - Math.max(this.heardAt, this.saidAt);
|
|
330
|
-
if (quiet > 2 *
|
|
332
|
+
if (quiet > 2 * KEEPALIVE_SECONDS * 1000) {
|
|
331
333
|
this.drop(generation, "Nothing came from the edge for a minute.");
|
|
332
334
|
}
|
|
333
|
-
},
|
|
335
|
+
}, KEEPALIVE_SECONDS * 500);
|
|
334
336
|
}
|
|
335
337
|
/** A socket given up on from this side: lost as a dropped network is (1006). */
|
|
336
338
|
drop(generation, reason) {
|
|
@@ -586,7 +588,7 @@ export class Conversation {
|
|
|
586
588
|
return false;
|
|
587
589
|
}
|
|
588
590
|
error(frame) {
|
|
589
|
-
if (frame.
|
|
591
|
+
if (frame.recording_id) {
|
|
590
592
|
this.refuse(frame);
|
|
591
593
|
return;
|
|
592
594
|
}
|
|
@@ -597,14 +599,14 @@ export class Conversation {
|
|
|
597
599
|
const pending = this.inFlight;
|
|
598
600
|
if (pending && !pending.started) {
|
|
599
601
|
this.inFlight = null;
|
|
600
|
-
pending.resolve(dropped("refused", frame.
|
|
602
|
+
pending.resolve(dropped("refused", frame.message));
|
|
601
603
|
this.update({ done: true });
|
|
602
604
|
this.sync();
|
|
603
605
|
}
|
|
604
606
|
}
|
|
605
607
|
/** An error naming a recording: it was refused, or failed. */
|
|
606
608
|
refuse(frame) {
|
|
607
|
-
const
|
|
609
|
+
const id = frame.recording_id;
|
|
608
610
|
const open = this.open;
|
|
609
611
|
if (open && open.id === id) {
|
|
610
612
|
open.failed = frame;
|
|
@@ -618,23 +620,23 @@ export class Conversation {
|
|
|
618
620
|
const pending = this.inFlight;
|
|
619
621
|
if (pending && pending.recording?.id === id && !pending.started) {
|
|
620
622
|
this.inFlight = null;
|
|
621
|
-
pending.resolve(dropped("refused", frame.
|
|
623
|
+
pending.resolve(dropped("refused", frame.message));
|
|
622
624
|
this.update({ done: true });
|
|
623
625
|
}
|
|
624
626
|
this.emit({ caption: null });
|
|
625
627
|
this.sync();
|
|
626
628
|
}
|
|
627
629
|
transcript(frame) {
|
|
628
|
-
// By its id; the oldest,
|
|
629
|
-
const recording = frame.
|
|
630
|
-
? this.hearing.find((r) => r.id === frame.
|
|
630
|
+
// By its id; the oldest, when it names none.
|
|
631
|
+
const recording = frame.recording_id
|
|
632
|
+
? this.hearing.find((r) => r.id === frame.recording_id)
|
|
631
633
|
: this.hearing[0];
|
|
632
634
|
if (recording) {
|
|
633
635
|
this.hearing = this.hearing.filter((r) => r !== recording);
|
|
634
636
|
recording.settle?.({
|
|
635
637
|
status: "done",
|
|
636
638
|
text: frame.text,
|
|
637
|
-
|
|
639
|
+
audioSeconds: frame.audio_seconds,
|
|
638
640
|
frame,
|
|
639
641
|
});
|
|
640
642
|
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -4,43 +4,51 @@
|
|
|
4
4
|
* caller acts on differently.
|
|
5
5
|
*
|
|
6
6
|
* The API answers its errors in one envelope —
|
|
7
|
-
* `{"error":{"
|
|
8
|
-
* (`apps/api/src/http/errors.ts`) — and `
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* - **Cloudflare's own refusal**, `error code: 1010` as plain text with no
|
|
13
|
-
* `X-Request-Id`: the request never reached the API, so there is no id
|
|
14
|
-
* to report.
|
|
7
|
+
* `{"error":{"code","message","request_id","issues"}}`
|
|
8
|
+
* (`apps/api/src/http/errors.ts`) — and `code` is the thing to switch on.
|
|
9
|
+
* One body is not that envelope, and is read here too: **Cloudflare's own
|
|
10
|
+
* refusal**, `error code: 1010` as plain text with no `X-Request-Id` — the
|
|
11
|
+
* request never reached the API, so there is no id to report.
|
|
15
12
|
*/
|
|
13
|
+
import type { ErrorCode, ValidationIssue } from "./gen/index.js";
|
|
14
|
+
export type { ErrorCode, ValidationIssue };
|
|
16
15
|
/** What one error says, as the SDK read it. */
|
|
17
16
|
export interface PlatformErrorInit {
|
|
18
17
|
status: number;
|
|
19
|
-
|
|
18
|
+
code: PlatformErrorCode;
|
|
20
19
|
message: string;
|
|
21
20
|
requestId: string | null;
|
|
22
|
-
|
|
21
|
+
issues?: ValidationIssue[];
|
|
23
22
|
retryAfter?: number | null;
|
|
24
23
|
cause?: unknown;
|
|
25
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* The API's codes, and the SDK's own for an answer that was not the API's:
|
|
27
|
+
* `cloudflare_error`, `http_error` (an error body that was not the
|
|
28
|
+
* envelope), `invalid_response` (a success that was not JSON),
|
|
29
|
+
* `connection_error` and `timeout` — and `client_error`, no request at
|
|
30
|
+
* all: the app's own code (its `getToken`) threw, as `@lumavox/react`
|
|
31
|
+
* reports it.
|
|
32
|
+
*/
|
|
33
|
+
export type PlatformErrorCode = ErrorCode | "cloudflare_error" | "http_error" | "invalid_response" | "connection_error" | "timeout" | "client_error";
|
|
26
34
|
export declare class PlatformError extends Error {
|
|
27
35
|
name: string;
|
|
28
36
|
/** The HTTP status; `0` when no response came back. */
|
|
29
37
|
readonly status: number;
|
|
30
|
-
/**
|
|
31
|
-
readonly
|
|
38
|
+
/** What went wrong: the API's `code` — `validation_failed`, `rate_limited`, … */
|
|
39
|
+
readonly code: PlatformErrorCode;
|
|
32
40
|
/**
|
|
33
41
|
* The request's `X-Request-Id` — the one the SDK sent, which the API
|
|
34
42
|
* keeps. `null` only when Cloudflare refused it before the API saw it.
|
|
35
43
|
*/
|
|
36
44
|
readonly requestId: string | null;
|
|
37
|
-
/**
|
|
38
|
-
readonly
|
|
45
|
+
/** Where a request failed validation; empty otherwise. */
|
|
46
|
+
readonly issues: ValidationIssue[];
|
|
39
47
|
/** Seconds to wait before trying again, from `Retry-After`; `null` without one. */
|
|
40
48
|
readonly retryAfter: number | null;
|
|
41
49
|
constructor(init: PlatformErrorInit);
|
|
42
50
|
}
|
|
43
|
-
/** 400 or 422: the request is malformed or failed validation; `
|
|
51
|
+
/** 400 or 422: the request is malformed or failed validation; `issues` says where. */
|
|
44
52
|
export declare class ValidationError extends PlatformError {
|
|
45
53
|
name: string;
|
|
46
54
|
}
|
|
@@ -60,7 +68,7 @@ export declare class PermissionError extends PlatformError {
|
|
|
60
68
|
export declare class NotFoundError extends PlatformError {
|
|
61
69
|
name: string;
|
|
62
70
|
}
|
|
63
|
-
/** 409: a plan's maximum,
|
|
71
|
+
/** 409: a plan's maximum, an id already taken, or a change since it was read. */
|
|
64
72
|
export declare class ConflictError extends PlatformError {
|
|
65
73
|
name: string;
|
|
66
74
|
}
|
package/dist/errors.js
CHANGED
|
@@ -1,18 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Every failure the SDK throws: a `PlatformError`, so one `catch` with
|
|
3
|
-
* `instanceof PlatformError` sees them all, and a subclass per status a
|
|
4
|
-
* caller acts on differently.
|
|
5
|
-
*
|
|
6
|
-
* The API answers its errors in one envelope —
|
|
7
|
-
* `{"error":{"type","message","request_id","details"?}}`
|
|
8
|
-
* (`apps/api/src/http/errors.ts`) — and `type` is the thing to switch on.
|
|
9
|
-
* Two bodies are not that envelope, and are read here too:
|
|
10
|
-
* - **the 400 the document declares** on every route with a body or a path
|
|
11
|
-
* parameter, `{status_code, detail, extra}`;
|
|
12
|
-
* - **Cloudflare's own refusal**, `error code: 1010` as plain text with no
|
|
13
|
-
* `X-Request-Id`: the request never reached the API, so there is no id
|
|
14
|
-
* to report.
|
|
15
|
-
*/
|
|
16
1
|
export class PlatformError extends Error {
|
|
17
2
|
constructor(init) {
|
|
18
3
|
// `init` is the options too: `Error` reads only its `cause`.
|
|
@@ -20,13 +5,13 @@ export class PlatformError extends Error {
|
|
|
20
5
|
// Written out, not `new.target.name`: a customer's minifier renames classes.
|
|
21
6
|
this.name = "PlatformError";
|
|
22
7
|
this.status = init.status;
|
|
23
|
-
this.
|
|
8
|
+
this.code = init.code;
|
|
24
9
|
this.requestId = init.requestId;
|
|
25
|
-
this.
|
|
10
|
+
this.issues = init.issues ?? [];
|
|
26
11
|
this.retryAfter = init.retryAfter ?? null;
|
|
27
12
|
}
|
|
28
13
|
}
|
|
29
|
-
/** 400 or 422: the request is malformed or failed validation; `
|
|
14
|
+
/** 400 or 422: the request is malformed or failed validation; `issues` says where. */
|
|
30
15
|
export class ValidationError extends PlatformError {
|
|
31
16
|
constructor() {
|
|
32
17
|
super(...arguments);
|
|
@@ -61,7 +46,7 @@ export class NotFoundError extends PlatformError {
|
|
|
61
46
|
this.name = "NotFoundError";
|
|
62
47
|
}
|
|
63
48
|
}
|
|
64
|
-
/** 409: a plan's maximum,
|
|
49
|
+
/** 409: a plan's maximum, an id already taken, or a change since it was read. */
|
|
65
50
|
export class ConflictError extends PlatformError {
|
|
66
51
|
constructor() {
|
|
67
52
|
super(...arguments);
|
|
@@ -132,34 +117,29 @@ export function errorFromResponse(status, headers, text, sentRequestId) {
|
|
|
132
117
|
body = undefined;
|
|
133
118
|
}
|
|
134
119
|
const e = isObject(body) && isObject(body.error) ? body.error : null;
|
|
135
|
-
const
|
|
120
|
+
const cloudflare = /^error code: (\d+)/.exec(text.trim())?.[1];
|
|
136
121
|
const snippet = text.trim().slice(0, 200);
|
|
137
122
|
const said = e
|
|
138
123
|
? {
|
|
139
|
-
|
|
124
|
+
code: typeof e.code === "string"
|
|
125
|
+
? e.code
|
|
126
|
+
: "http_error",
|
|
140
127
|
message: typeof e.message === "string" ? e.message : http,
|
|
141
128
|
requestId: typeof e.request_id === "string" ? e.request_id : requestId,
|
|
142
|
-
|
|
129
|
+
issues: Array.isArray(e.issues) ? e.issues : [],
|
|
143
130
|
}
|
|
144
|
-
:
|
|
131
|
+
: cloudflare && answered === null
|
|
145
132
|
? {
|
|
146
|
-
|
|
147
|
-
message:
|
|
148
|
-
|
|
149
|
-
|
|
133
|
+
code: "cloudflare_error",
|
|
134
|
+
message: `Cloudflare refused the request with error code ${cloudflare} ` +
|
|
135
|
+
"before it reached the API.",
|
|
136
|
+
requestId: null,
|
|
150
137
|
}
|
|
151
|
-
:
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
requestId: null,
|
|
157
|
-
}
|
|
158
|
-
: {
|
|
159
|
-
type: "http_error",
|
|
160
|
-
message: snippet ? `${http}: ${snippet}` : http,
|
|
161
|
-
requestId,
|
|
162
|
-
};
|
|
138
|
+
: {
|
|
139
|
+
code: "http_error",
|
|
140
|
+
message: snippet ? `${http}: ${snippet}` : http,
|
|
141
|
+
requestId,
|
|
142
|
+
};
|
|
163
143
|
return new (classOf(status))({
|
|
164
144
|
...said,
|
|
165
145
|
status,
|
package/dist/events.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Every frame the edge sends, as it sends it: the wire's own field names,
|
|
3
3
|
* snake_case included, so the docs' WebSocket page and these types read
|
|
4
|
-
* the same.
|
|
4
|
+
* the same — and every field always there, `null` where it does not apply.
|
|
5
5
|
*
|
|
6
6
|
* **Hand-written, and held twice.** `ServerFrames` is keyed by the event
|
|
7
7
|
* names `protocol.gen.ts` takes from the edge's vocabulary, so an event the
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* test (`apps/edge/test/sdk.test.ts`) holds every field of every frame the
|
|
10
10
|
* real conversation object sends to these, both ways.
|
|
11
11
|
*/
|
|
12
|
-
import type { RelationInfo } from "./gen/index.js";
|
|
12
|
+
import type { ErrorCode, RelationInfo } from "./gen/index.js";
|
|
13
13
|
import type { ServerEventName } from "./protocol.gen.js";
|
|
14
14
|
interface Frame<T extends ServerEventName> {
|
|
15
15
|
type: T;
|
|
@@ -51,7 +51,8 @@ export interface TurnStartFrame extends Frame<"turn.start">, InTurn {
|
|
|
51
51
|
/** A spoken turn's transcript, before the reply. */
|
|
52
52
|
export interface HeardFrame extends Frame<"heard">, InTurn {
|
|
53
53
|
text: string;
|
|
54
|
-
|
|
54
|
+
/** How long the recording was. */
|
|
55
|
+
audio_seconds: number;
|
|
55
56
|
}
|
|
56
57
|
/** The open recording as heard so far: a caption, replaced by the next. */
|
|
57
58
|
export interface HeardPartialFrame extends Frame<"heard.partial"> {
|
|
@@ -60,10 +61,11 @@ export interface HeardPartialFrame extends Frame<"heard.partial"> {
|
|
|
60
61
|
/** A `transcribeOnly` recording's words: part of no turn. */
|
|
61
62
|
export interface TranscriptFrame extends Frame<"transcript"> {
|
|
62
63
|
text: string;
|
|
63
|
-
|
|
64
|
+
/** How long the recording was. */
|
|
65
|
+
audio_seconds: number;
|
|
64
66
|
request_id: string;
|
|
65
67
|
/** The recording's id, which the SDK names every recording with. */
|
|
66
|
-
|
|
68
|
+
recording_id: string | null;
|
|
67
69
|
}
|
|
68
70
|
export interface MoodFrame extends Frame<"mood">, InTurn {
|
|
69
71
|
mood: string;
|
|
@@ -109,7 +111,7 @@ export interface AudioStartFrame extends Frame<"audio.start">, InTurn, AudioForm
|
|
|
109
111
|
export interface AudioEndFrame extends Frame<"audio.end">, InTurn {
|
|
110
112
|
chunk: number;
|
|
111
113
|
bytes: number;
|
|
112
|
-
|
|
114
|
+
audio_seconds: number;
|
|
113
115
|
}
|
|
114
116
|
/** After `mute`, once the last audio window has closed. */
|
|
115
117
|
export interface MutedFrame extends Frame<"muted">, InTurn {
|
|
@@ -150,10 +152,8 @@ export interface TurnDoneFrame extends Frame<"turn.done">, InTurn {
|
|
|
150
152
|
output_tokens: number | null;
|
|
151
153
|
mood: string | null;
|
|
152
154
|
mood_intensity: number | null;
|
|
153
|
-
/**
|
|
154
|
-
|
|
155
|
-
/** `true` when the reply was cut short. */
|
|
156
|
-
truncated: true | null;
|
|
155
|
+
/** Whether the reply was cut short. */
|
|
156
|
+
truncated: boolean;
|
|
157
157
|
chunks_spoken: number;
|
|
158
158
|
heard: string | null;
|
|
159
159
|
heard_seconds: number | null;
|
|
@@ -164,22 +164,27 @@ export interface InterruptedFrame extends Frame<"interrupted">, InTurn {
|
|
|
164
164
|
message: string;
|
|
165
165
|
mood: string | null;
|
|
166
166
|
mood_intensity: number | null;
|
|
167
|
-
|
|
167
|
+
/** Why it stopped: `cancel`, or the failure its `error` named. */
|
|
168
|
+
reason: string | null;
|
|
168
169
|
}
|
|
169
170
|
/**
|
|
170
171
|
* Something went wrong. It does not end the connection, and inside a turn
|
|
171
|
-
* it does not end the turn. `
|
|
172
|
-
*
|
|
173
|
-
* `
|
|
172
|
+
* it does not end the turn. `code` is the REST API's (`conflict` for a
|
|
173
|
+
* turn already running, `insufficient_credits`, `provider_failed`, …);
|
|
174
|
+
* `stage: "speech"` when only the speaking failed.
|
|
174
175
|
*/
|
|
175
176
|
export interface ErrorFrame extends Frame<"error"> {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
177
|
+
code: ErrorCode;
|
|
178
|
+
message: string;
|
|
179
|
+
/** The turn it ended or interrupted. */
|
|
180
|
+
turn: string | null;
|
|
181
|
+
stage: "speech" | null;
|
|
182
|
+
/** The reply's chunk that could not be spoken. */
|
|
183
|
+
chunk: number | null;
|
|
184
|
+
/** A `transcribeOnly` recording's request. */
|
|
185
|
+
request_id: string | null;
|
|
181
186
|
/** The recording it refused or failed. */
|
|
182
|
-
|
|
187
|
+
recording_id: string | null;
|
|
183
188
|
}
|
|
184
189
|
/** The answer to `ping`, and sent unprompted after 30 s of client silence. */
|
|
185
190
|
export interface PongFrame extends Frame<"pong"> {
|