@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 +21 -0
- package/README.md +119 -2
- package/dist/audio.d.ts +38 -0
- package/dist/audio.js +1 -0
- package/dist/client.d.ts +108 -0
- package/dist/client.js +122 -0
- package/dist/conversation.d.ts +324 -0
- package/dist/conversation.js +763 -0
- package/dist/errors.d.ts +86 -0
- package/dist/errors.js +168 -0
- package/dist/events.d.ts +224 -0
- package/dist/events.js +1 -0
- package/dist/gen/index.d.ts +1 -0
- package/dist/gen/index.js +2 -0
- package/dist/gen/types.gen.d.ts +776 -0
- package/dist/gen/types.gen.js +2 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +15 -0
- package/dist/pcm.d.ts +38 -0
- package/dist/pcm.js +110 -0
- package/dist/protocol.gen.d.ts +44 -0
- package/dist/protocol.gen.js +46 -0
- package/dist/socket.d.ts +167 -0
- package/dist/socket.js +185 -0
- package/dist/token.d.ts +39 -0
- package/dist/token.js +63 -0
- package/dist/transport.d.ts +83 -0
- package/dist/transport.js +236 -0
- package/package.json +39 -4
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
|
-
#
|
|
1
|
+
# @lumavox/client
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
package/dist/audio.d.ts
ADDED
|
@@ -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 {};
|
package/dist/client.d.ts
ADDED
|
@@ -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
|
+
}
|