@lumavox/client 0.0.0-stage → 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lumavox
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,120 @@
1
- # Temporary Holding Version
1
+ # @lumavox/client
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Talk to Lumavox agents from an end user's device — a browser, a React
4
+ Native app, Node — with **their ephemeral key**, never your API key: a
5
+ conversation on the agent's socket that keeps itself connected, and the
6
+ agent's REST routes for sessions, memory and relationships. **Zero
7
+ dependencies.**
8
+
9
+ ```sh
10
+ npm install @lumavox/client
11
+ ```
12
+
13
+ ```ts
14
+ import { createClient } from "@lumavox/client";
15
+
16
+ const lumavox = createClient({
17
+ // Your server mints the key with your API key (POST /v1/ephemeral-keys).
18
+ getToken: async () => (await fetch("/api/lumavox-key")).text(),
19
+ });
20
+
21
+ const session = await lumavox.sessions.create({ agent_id: "agt_..." });
22
+
23
+ const conversation = lumavox.connect(session.id, {
24
+ onText: ({ text }) => render(text),
25
+ onDone: ({ message }) => save(message),
26
+ });
27
+
28
+ const outcome = await conversation.send("What should I see on my first morning?");
29
+ // { status: "done", frame } | { status: "interrupted", frame } | { status: "dropped", reason }
30
+ ```
31
+
32
+ React and React Native: `@lumavox/react`, hooks over this.
33
+
34
+ The guide, with every example: the docs' **SDKs** tab, *Client SDK*.
35
+
36
+ ## The client
37
+
38
+ ```ts
39
+ createClient({
40
+ getToken, // ({ reason, status?, code? }) => key; throw to stop
41
+ baseUrl: "https://api.lumavox.ai", // the default
42
+ socketUrl, // baseUrl as wss:, plus /v1/agent/stream, by default
43
+ timeoutMs: 30_000, // per attempt
44
+ maxRetries: 2,
45
+ fetch, // the runtime's own by default
46
+ WebSocket, // the runtime's own by default; `ws` on Node before 22
47
+ });
48
+ ```
49
+
50
+ **The key** is asked for once, kept, and replaced once when the platform
51
+ refuses it — an HTTP 401, or the socket closed with 4002 to 4005 — however
52
+ many calls were refused with it: every mint spends credit up front. Never
53
+ for a 402, 403 or 429. `clearToken()` at sign-out.
54
+
55
+ | Resource | Methods |
56
+ |---|---|
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` |
60
+
61
+ Bodies and answers are the API's own JSON, as the API reference shows them;
62
+ the SDK's options go in a last argument: `{ timeoutMs, signal, maxRetries,
63
+ requestId }`. A delete of what is already gone answers `null`. Errors are
64
+ `PlatformError` and its subclasses — `AuthenticationError`,
65
+ `PermissionError`, `NotFoundError`, `RateLimitError`, … — with `status`,
66
+ `type`, `requestId`, `details` and `retryAfter`, as `@lumavox/server`'s.
67
+
68
+ ## A conversation
69
+
70
+ `connect(sessionId, handlers, options)` answers at once. It asks for the
71
+ key, dials, authenticates, and from then on:
72
+
73
+ - **queues** what you send behind the agent's greeting, a running turn, a
74
+ reconnect or a quiet spell; `send` answers how the turn ended and never
75
+ rejects;
76
+ - **reconnects**: a new key once for 4002–4005, 5 s backoff for a rate or
77
+ connection limit, 0.5 s for anything else, ±20 % jitter, final after
78
+ five in a row — and **rests** after the edge hangs up on a quiet
79
+ connection (4007), dialling again on the next `send`;
80
+ - **watches**: 60 s with nothing from the edge is a dead socket, dropped
81
+ and dialled again;
82
+ - publishes `snapshot` — `state`, `ready`, the `turn` (its text, chunks,
83
+ mood, tool, knowledge), `caption`, `relation`, `queued` — with
84
+ `subscribe(fn)`.
85
+
86
+ | Handler | Frame |
87
+ |---|---|
88
+ | `onReady` `onTurnStart` `onHeard` `onHeardPartial` `onKnowledge` `onMood` `onThought` `onText` `onChunk` `onToolStart` `onToolEnd` `onUsage` `onMuted` `onAudioStart` `onAudioEnd` `onTranscript` `onDone` `onInterrupted` `onError` | the wire's frame, as it is |
89
+ | `onAudio` | `{ pcm, chunk, turn, sample_rate, channels }`: raw 16-bit PCM, never copied |
90
+ | `onRelation` | the relationship, each change once |
91
+ | `onState` | `connecting` `ready` `busy` `reconnecting` `dormant` `closed` |
92
+ | `onDisconnect` | every socket that closed, recovered from or not: `{ code, reason, ready }` |
93
+ | `onClose` | `{ code, reason, final }`, only when it will not reconnect |
94
+ | `onWarning` | a gap in the frames, one it could not read, audio past 10 MB |
95
+ | `onEvent` | every frame |
96
+
97
+ With no `onAudio` handler every turn asks for no speech, so nothing pays
98
+ for audio nobody hears; `connect`'s `{ speech }` overrides that.
99
+
100
+ ### Speaking
101
+
102
+ ```ts
103
+ conversation.startAudio({ rate: 16_000 }); // one of RECORDING_RATES
104
+ conversation.sendAudio(pcm); // Int16Array or ArrayBuffer of 16-bit LE PCM
105
+ const turn = await conversation.endAudio(); // a spoken turn
106
+ // …or the words alone, for this recording, however others finish:
107
+ const words = await conversation.endAudio({ transcribeOnly: true });
108
+ ```
109
+
110
+ The SDK captures and plays nothing itself: a recorder hands it samples, a
111
+ player takes `onAudio`'s. `floatToPcm16`, `pcm16ToFloat`, `resample`,
112
+ `concatPcm` and `pcmSeconds` convert between them and the wire;
113
+ `AgentAudio` is the interface `@lumavox/react` drives.
114
+
115
+ ## Runtimes
116
+
117
+ Browsers, React Native (Hermes, ES2020 — and each binary frame reaches JS
118
+ as base64, a cost per audio frame no SDK can remove), Node 22 and later.
119
+
120
+ MIT, copyright Lumavox.
@@ -0,0 +1,38 @@
1
+ /**
2
+ * What an app plugs in to make a conversation audible and to speak in it:
3
+ * a player and a recorder. **Types only in v1**: the SDK captures and
4
+ * plays nothing itself, so it adds no audio library to a customer's app —
5
+ * the console's Web Audio player and recorder are the first implementation
6
+ * (`apps/console/src/components/audio/agent-audio.ts`), and
7
+ * `@lumavox/react`'s `useAgent` drives one: it plays every audio frame,
8
+ * stops on a cancel, a mute or a new turn, and records at the rate the
9
+ * recorder was granted. Voice activity detection stays the app's.
10
+ */
11
+ export interface AgentAudio {
12
+ /** Queue one frame of a reply: 16-bit little-endian PCM, in wire order. */
13
+ play(pcm: ArrayBuffer, frame: {
14
+ sample_rate: number;
15
+ channels: number;
16
+ chunk: number;
17
+ turn: string;
18
+ }): void;
19
+ /** Drop everything queued and stop: a cancel, a mute, a barge-in. */
20
+ stop(): void;
21
+ /** Called when the queue has played out: the agent stopped speaking. Answers the unsubscribe. */
22
+ onDrained(fn: () => void): () => void;
23
+ /**
24
+ * Open the microphone and call `onPcm` with 16-bit little-endian PCM as
25
+ * it is captured. Answers the rate it was granted, which must be one of
26
+ * `RECORDING_RATES`: a recorder that cannot have its asked rate
27
+ * resamples (`resample`) or refuses.
28
+ */
29
+ startCapture(onPcm: (pcm: ArrayBuffer) => void, options?: {
30
+ rate?: number;
31
+ }): Promise<{
32
+ rate: number;
33
+ }>;
34
+ /** Close the microphone; the last samples are delivered before it settles. */
35
+ stopCapture(): Promise<void>;
36
+ /** Free everything: the conversation is over. */
37
+ dispose(): void;
38
+ }
package/dist/audio.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,108 @@
1
+ /**
2
+ * `createClient`: an end user's ephemeral key (`getToken`), one base URL,
3
+ * the agent's REST routes — sessions, facts, relations, the ten
4
+ * `/v1/agent/*` operations (`test/coverage.test.ts` holds the document to
5
+ * them) — and `connect`, the agent's socket (`conversation.ts`).
6
+ *
7
+ * - **Shaped as the server SDK is** (`sdks/server`): bodies and answers in
8
+ * the wire's snake_case, camelCase methods, the SDK's options
9
+ * (`timeoutMs`, `signal`, `maxRetries`, `requestId`) last; `list()` every
10
+ * item, `page()` one page; a delete of what is already gone answers
11
+ * `null`; the same `transport.ts` and `errors.ts`, copied.
12
+ * - **A key is asked for once, and replaced once**: a call refused with 401
13
+ * asks for a new key and is sent again; a second refusal, on the fresh
14
+ * key, is the caller's (`token.ts`). **Never on 402, 403 or 429**: on this
15
+ * API 403 is a key bound to another agent and 429 a window, and a new key
16
+ * there would spend credit to dodge a limit.
17
+ * - **`end_user_id` is never sent**: the key names the person.
18
+ * - **Nothing is read from the environment**, and the runtime's own
19
+ * `fetch` and `WebSocket` are the defaults, looked up when used: Node 22
20
+ * and later, browsers, React Native.
21
+ */
22
+ import { type ConnectOptions, Conversation, type ConversationHandlers } from "./conversation.js";
23
+ import type { CompactionInfo, CompactRequest, FactList, ForgottenFacts, MessageInfo, RelationList, SessionCreateRequest, SessionDetail, SessionInfo } from "./gen/index.js";
24
+ import type { WebSocketConstructor } from "./socket.js";
25
+ import { type GetToken } from "./token.js";
26
+ import { type Fetch, type RequestOptions } from "./transport.js";
27
+ export declare const DEFAULT_BASE_URL = "https://api.lumavox.ai";
28
+ export interface ClientOptions {
29
+ /**
30
+ * The end user's ephemeral key, from your server — which mints it with
31
+ * your API key (`POST /v1/ephemeral-keys`). Called once, and again only
32
+ * when the platform refuses the key it gave; throw to stop.
33
+ */
34
+ getToken: GetToken;
35
+ /** `https://api.lumavox.ai` by default; staging is `https://stg-api.lumavox.ai`. */
36
+ baseUrl?: string;
37
+ /** The agent's socket: `baseUrl` as `wss:`, plus `/v1/agent/stream`, by default. */
38
+ socketUrl?: string;
39
+ /** Per attempt, in milliseconds: 30 s by default. */
40
+ timeoutMs?: number;
41
+ /** Retries of a call that can be sent again safely: 2 by default. */
42
+ maxRetries?: number;
43
+ /** The `fetch` to call: the runtime's own by default. */
44
+ fetch?: Fetch;
45
+ /** The `WebSocket` class: the runtime's own by default; `ws` on an older Node. */
46
+ WebSocket?: WebSocketConstructor;
47
+ }
48
+ export interface SessionListParams {
49
+ /** How many a page holds, up to 200. */
50
+ limit?: number;
51
+ offset?: number;
52
+ }
53
+ export interface SessionGetParams {
54
+ /** Messages, counting back from the newest: 50 by default, up to 200. */
55
+ limit?: number;
56
+ /** Only messages before this `seq`: the last page's `next_before`. */
57
+ before?: number;
58
+ }
59
+ export interface Sessions {
60
+ /** Open a conversation with an agent — or resume the person's last, by default (`resume`). */
61
+ create(body: SessionCreateRequest, options?: RequestOptions): Promise<SessionDetail>;
62
+ /** Every one of the person's conversations, newest first, page after page. */
63
+ list(params?: SessionListParams, options?: RequestOptions): AsyncGenerator<SessionInfo, void, undefined>;
64
+ /** One page of them. */
65
+ page(params?: SessionListParams, options?: RequestOptions): Promise<SessionInfo[]>;
66
+ /** One conversation, with a page of its transcript. */
67
+ get(id: string, params?: SessionGetParams, options?: RequestOptions): Promise<SessionDetail>;
68
+ /** Its whole transcript, newest message first, paging back by `next_before`. */
69
+ history(id: string, options?: RequestOptions): AsyncGenerator<MessageInfo, void, undefined>;
70
+ /** Delete it; `null` when it is already gone. */
71
+ delete(id: string, options?: RequestOptions): Promise<SessionInfo | null>;
72
+ /** Summarise it now; `extract_facts` also stores what is worth remembering. */
73
+ compact(id: string, body?: CompactRequest, options?: RequestOptions): Promise<CompactionInfo>;
74
+ /** Compact it and extract facts, in one model call. */
75
+ extract(id: string, options?: RequestOptions): Promise<CompactionInfo>;
76
+ }
77
+ export interface Facts {
78
+ /** What is remembered about the person: the first 100, as the route answers (left-overs #109). */
79
+ list(options?: RequestOptions): Promise<FactList>;
80
+ /** Forget one; `null` when it is already gone. */
81
+ delete(id: string, options?: RequestOptions): Promise<ForgottenFacts | null>;
82
+ /** Forget everything about the person. Their conversations stay. */
83
+ deleteAll(options?: RequestOptions): Promise<ForgottenFacts>;
84
+ }
85
+ export interface RelationListParams {
86
+ /** One agent's row alone. */
87
+ agent_id?: string;
88
+ /** Up to 50 by default; clamped to the route's cap, which `total` shows. */
89
+ limit?: number;
90
+ }
91
+ export interface Relations {
92
+ /** How the agents the person talked to feel about them, most recent first. */
93
+ list(params?: RelationListParams, options?: RequestOptions): Promise<RelationList>;
94
+ }
95
+ export interface Client {
96
+ readonly sessions: Sessions;
97
+ readonly facts: Facts;
98
+ readonly relations: Relations;
99
+ /**
100
+ * Join conversation `sessionId` on the agent's socket. Answers at once:
101
+ * the conversation dials, authenticates and keeps itself connected, and
102
+ * its handlers and `snapshot` say how it goes.
103
+ */
104
+ connect(sessionId: string, handlers?: ConversationHandlers, options?: ConnectOptions): Conversation;
105
+ /** Forget the key — at sign-out. The next call asks `getToken` again. */
106
+ clearToken(): void;
107
+ }
108
+ export declare function createClient(options: ClientOptions): Client;
package/dist/client.js ADDED
@@ -0,0 +1,122 @@
1
+ /**
2
+ * `createClient`: an end user's ephemeral key (`getToken`), one base URL,
3
+ * the agent's REST routes — sessions, facts, relations, the ten
4
+ * `/v1/agent/*` operations (`test/coverage.test.ts` holds the document to
5
+ * them) — and `connect`, the agent's socket (`conversation.ts`).
6
+ *
7
+ * - **Shaped as the server SDK is** (`sdks/server`): bodies and answers in
8
+ * the wire's snake_case, camelCase methods, the SDK's options
9
+ * (`timeoutMs`, `signal`, `maxRetries`, `requestId`) last; `list()` every
10
+ * item, `page()` one page; a delete of what is already gone answers
11
+ * `null`; the same `transport.ts` and `errors.ts`, copied.
12
+ * - **A key is asked for once, and replaced once**: a call refused with 401
13
+ * asks for a new key and is sent again; a second refusal, on the fresh
14
+ * key, is the caller's (`token.ts`). **Never on 402, 403 or 429**: on this
15
+ * API 403 is a key bound to another agent and 429 a window, and a new key
16
+ * there would spend credit to dodge a limit.
17
+ * - **`end_user_id` is never sent**: the key names the person.
18
+ * - **Nothing is read from the environment**, and the runtime's own
19
+ * `fetch` and `WebSocket` are the defaults, looked up when used: Node 22
20
+ * and later, browsers, React Native.
21
+ */
22
+ import { Conversation, } from "./conversation.js";
23
+ import { AuthenticationError } from "./errors.js";
24
+ import { Tokens } from "./token.js";
25
+ import { orNull, segment, Transport, } from "./transport.js";
26
+ export const DEFAULT_BASE_URL = "https://api.lumavox.ai";
27
+ /** A page as large as the route allows: fewer calls for a long list. */
28
+ const PAGE = 200;
29
+ export function createClient(options) {
30
+ if (typeof options?.getToken !== "function") {
31
+ throw new TypeError("createClient needs getToken: a function answering the end user's ephemeral key.");
32
+ }
33
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
34
+ const socketBase = options.socketUrl ?? `${baseUrl.replace(/^http/, "ws")}/v1/agent/stream`;
35
+ const own = options.fetch;
36
+ const transport = new Transport({
37
+ apiKey: "",
38
+ baseUrl,
39
+ timeoutMs: options.timeoutMs ?? 30000,
40
+ maxRetries: options.maxRetries ?? 2,
41
+ fetch: (url, init) => (own ?? fetch)(url, init),
42
+ });
43
+ const tokens = new Tokens(options.getToken);
44
+ /** A call with the key, and with a new one once if it is refused. */
45
+ async function send(call, request) {
46
+ const key = await tokens.current();
47
+ try {
48
+ return (await transport.request({ ...call, token: key }, request))
49
+ .data;
50
+ }
51
+ catch (error) {
52
+ if (!(error instanceof AuthenticationError))
53
+ throw error;
54
+ const fresh = await tokens.replace(key, {
55
+ status: 401,
56
+ code: error.type,
57
+ });
58
+ return (await transport.request({ ...call, token: fresh }, request))
59
+ .data;
60
+ }
61
+ }
62
+ const sessionPath = (id, rest = "") => `/v1/agent/sessions/${segment(id)}${rest}`;
63
+ const page = (params = {}, request) => send({ method: "GET", path: "/v1/agent/sessions", query: { ...params } }, request);
64
+ const get = (id, params = {}, request) => send({ method: "GET", path: sessionPath(id), query: { ...params } }, request);
65
+ return {
66
+ sessions: {
67
+ create: (body, request) => send({ method: "POST", path: "/v1/agent/sessions", body }, request),
68
+ async *list(params = {}, request) {
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
+ },
79
+ page,
80
+ get,
81
+ async *history(id, request) {
82
+ let before;
83
+ do {
84
+ const detail = await get(id, { limit: PAGE, before: before ?? undefined }, request);
85
+ for (let i = detail.messages.length - 1; i >= 0; i--) {
86
+ yield detail.messages[i];
87
+ }
88
+ before = detail.next_before;
89
+ } while (before !== null && before !== undefined);
90
+ },
91
+ delete: (id, request) => orNull(send({ method: "DELETE", path: sessionPath(id) }, request)),
92
+ // The document requires a body; none is `{}`, the agent's own setting.
93
+ compact: (id, body = {}, request) => send({ method: "POST", path: sessionPath(id, "/compact"), body }, request),
94
+ extract: (id, request) => send({ method: "POST", path: sessionPath(id, "/extract") }, request),
95
+ },
96
+ facts: {
97
+ list: (request) => send({ method: "GET", path: "/v1/agent/facts" }, request),
98
+ delete: (id, request) => orNull(send({ method: "DELETE", path: `/v1/agent/facts/${segment(id)}` }, request)),
99
+ deleteAll: (request) => send({ method: "DELETE", path: "/v1/agent/facts" }, request),
100
+ },
101
+ relations: {
102
+ list: (params = {}, request) => send({ method: "GET", path: "/v1/agent/relations", query: { ...params } }, request),
103
+ },
104
+ connect(sessionId, handlers = {}, connectOptions = {}) {
105
+ const WebSocket = options.WebSocket ??
106
+ globalThis
107
+ .WebSocket;
108
+ if (!WebSocket) {
109
+ throw new TypeError("This runtime has no WebSocket: pass createClient({ WebSocket }), from the ws package on Node before 22.");
110
+ }
111
+ return new Conversation({
112
+ sessionId,
113
+ socketUrl: socketBase,
114
+ tokens,
115
+ WebSocket,
116
+ handlers,
117
+ options: connectOptions,
118
+ });
119
+ },
120
+ clearToken: () => tokens.clear(),
121
+ };
122
+ }