@lumavox/client 0.5.0 → 0.6.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 +11 -0
- package/dist/conversation.d.ts +22 -1
- package/dist/conversation.js +47 -1
- package/dist/events.d.ts +14 -0
- package/dist/index.d.ts +1 -1
- package/dist/protocol.gen.d.ts +4 -2
- package/dist/protocol.gen.js +4 -2
- package/dist/socket.d.ts +2 -0
- package/dist/socket.js +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -105,6 +105,17 @@ A tool marked "ask first" sends `tool.confirm` and does not run:
|
|
|
105
105
|
`confirmTool(callId, yes)` answers with the person's tap — a turn of
|
|
106
106
|
theirs, queued as `send` is, which runs the tool first on a yes.
|
|
107
107
|
|
|
108
|
+
The agent's app tools run in your app: `connect`'s `tools` gives a
|
|
109
|
+
handler for each, by name. The conversation names them when it
|
|
110
|
+
authenticates, runs one when the model calls it, and sends back what it
|
|
111
|
+
answers — or what it threw:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
const conversation = client.connect(sessionId, {}, {
|
|
115
|
+
tools: { open_page: async ({ page }) => ({ opened: page }) },
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
108
119
|
### Speaking
|
|
109
120
|
|
|
110
121
|
```ts
|
package/dist/conversation.d.ts
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* shows — a `text.delta`, say — and none for audio, which is passed
|
|
36
36
|
* through, never copied.
|
|
37
37
|
*/
|
|
38
|
-
import type { ErrorFrame, InterruptedFrame, KnowledgeFrame, ReadyFrame, ServerFrame, ServerFrames, TranscriptFrame, TurnDoneFrame } from "./events.js";
|
|
38
|
+
import type { ErrorFrame, InterruptedFrame, KnowledgeFrame, ReadyFrame, ServerFrame, ServerFrames, ToolCallFrame, TranscriptFrame, TurnDoneFrame } from "./events.js";
|
|
39
39
|
import type { RelationInfo } from "./gen/index.js";
|
|
40
40
|
import { type AudioFrame, type Warning, type WebSocketConstructor } from "./socket.js";
|
|
41
41
|
import type { Tokens } from "./token.js";
|
|
@@ -169,6 +169,12 @@ export interface ConversationHandlers {
|
|
|
169
169
|
/** Every frame, the ones above included: what a newer edge sends that this SDK does not know. */
|
|
170
170
|
onEvent?: (frame: ServerFrame) => void;
|
|
171
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* One of your app's tools: given the model's arguments, it answers any
|
|
174
|
+
* JSON (or nothing, `null`), maybe as a promise. A throw or a rejection is
|
|
175
|
+
* the call's `error`, which the model is told.
|
|
176
|
+
*/
|
|
177
|
+
export type ToolHandler = (args: Record<string, unknown>, call: ToolCallFrame) => unknown;
|
|
172
178
|
export interface ConnectOptions {
|
|
173
179
|
/**
|
|
174
180
|
* Every turn's `speech`. By default `false` when there is no `onAudio`
|
|
@@ -178,6 +184,12 @@ export interface ConnectOptions {
|
|
|
178
184
|
speech?: boolean;
|
|
179
185
|
/** Ask every turn for thinking summaries (`onThought`). */
|
|
180
186
|
thoughts?: boolean;
|
|
187
|
+
/**
|
|
188
|
+
* The app's own tools, by name: the agent is offered those of its tools
|
|
189
|
+
* of kind `app` named here, and each call runs its handler here. At most
|
|
190
|
+
* `APP_TOOLS_MAX` (50).
|
|
191
|
+
*/
|
|
192
|
+
tools?: Record<string, ToolHandler>;
|
|
181
193
|
}
|
|
182
194
|
/** Per turn: overrides `ConnectOptions`. */
|
|
183
195
|
export interface TurnOptions {
|
|
@@ -308,6 +320,15 @@ export declare class Conversation {
|
|
|
308
320
|
private stopTimers;
|
|
309
321
|
private frame;
|
|
310
322
|
private ready;
|
|
323
|
+
/**
|
|
324
|
+
* One of the app's tools, called by the model: its handler run, and its
|
|
325
|
+
* answer — or what it threw — sent back as `tool.result` on the socket
|
|
326
|
+
* the call came on. A reconnect in between ended the call at the edge,
|
|
327
|
+
* so nothing is sent on the new socket. An action-only call is answered
|
|
328
|
+
* by nobody: its handler runs, and one that throws is rethrown on its
|
|
329
|
+
* own, as any handler's is.
|
|
330
|
+
*/
|
|
331
|
+
private runTool;
|
|
311
332
|
/** The end of a turn: answers whether it was a second for the same turn, which says nothing new. */
|
|
312
333
|
private terminal;
|
|
313
334
|
private error;
|
package/dist/conversation.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { wireBytes } from "./pcm.js";
|
|
2
|
-
import { KEEPALIVE_SECONDS, MAX_AUDIO_BYTES, MAX_MESSAGE_CHARS, RECORDING_RATES, } from "./protocol.gen.js";
|
|
2
|
+
import { KEEPALIVE_SECONDS, MAX_AUDIO_BYTES, MAX_MESSAGE_CHARS, RECORDING_RATES, TOOL_ERROR_MAX_CHARS, } 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;
|
|
@@ -315,6 +315,7 @@ export class Conversation {
|
|
|
315
315
|
url: socketUrl(this.setup.socketUrl, this.sessionId),
|
|
316
316
|
key,
|
|
317
317
|
sessionId: this.sessionId,
|
|
318
|
+
tools: Object.keys(this.setup.options.tools ?? {}),
|
|
318
319
|
WebSocket: this.setup.WebSocket,
|
|
319
320
|
onFrame: (frame) => this.frame(frame),
|
|
320
321
|
onAudio: (frame) => {
|
|
@@ -508,6 +509,9 @@ export class Conversation {
|
|
|
508
509
|
case "tool.end":
|
|
509
510
|
this.update({ tool: null });
|
|
510
511
|
break;
|
|
512
|
+
case "tool.call":
|
|
513
|
+
this.runTool(frame);
|
|
514
|
+
break;
|
|
511
515
|
case "knowledge":
|
|
512
516
|
this.update({
|
|
513
517
|
knowledge: [...(this.snap.turn?.knowledge ?? []), frame],
|
|
@@ -563,6 +567,48 @@ export class Conversation {
|
|
|
563
567
|
}
|
|
564
568
|
this.sync();
|
|
565
569
|
}
|
|
570
|
+
/**
|
|
571
|
+
* One of the app's tools, called by the model: its handler run, and its
|
|
572
|
+
* answer — or what it threw — sent back as `tool.result` on the socket
|
|
573
|
+
* the call came on. A reconnect in between ended the call at the edge,
|
|
574
|
+
* so nothing is sent on the new socket. An action-only call is answered
|
|
575
|
+
* by nobody: its handler runs, and one that throws is rethrown on its
|
|
576
|
+
* own, as any handler's is.
|
|
577
|
+
*/
|
|
578
|
+
runTool(frame) {
|
|
579
|
+
const handler = this.setup.options.tools?.[frame.name];
|
|
580
|
+
const socket = this.socket;
|
|
581
|
+
const answer = (fields) => {
|
|
582
|
+
if (frame.action_only || !socket || this.socket !== socket)
|
|
583
|
+
return;
|
|
584
|
+
const sent = { type: "tool.result", call_id: frame.call_id };
|
|
585
|
+
try {
|
|
586
|
+
socket.send({ ...sent, ...fields });
|
|
587
|
+
}
|
|
588
|
+
catch {
|
|
589
|
+
// swallowed: a result JSON cannot carry (a cycle, a BigInt), told
|
|
590
|
+
// to the model as the failure it is.
|
|
591
|
+
socket.send({ ...sent, error: "The tool's answer is not JSON." });
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
new Promise((resolve) => {
|
|
595
|
+
if (!handler)
|
|
596
|
+
throw new Error(`This app has no tool ${frame.name}.`);
|
|
597
|
+
resolve(handler(frame.arguments, frame));
|
|
598
|
+
}).then((result) => answer({ result: result === undefined ? null : result }), (error) => {
|
|
599
|
+
const said = error instanceof Error ? error.message : String(error);
|
|
600
|
+
answer({
|
|
601
|
+
error: [...(said || "The tool failed.")]
|
|
602
|
+
.slice(0, TOOL_ERROR_MAX_CHARS)
|
|
603
|
+
.join(""),
|
|
604
|
+
});
|
|
605
|
+
if (frame.action_only) {
|
|
606
|
+
setTimeout(() => {
|
|
607
|
+
throw error;
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
}
|
|
566
612
|
/** The end of a turn: answers whether it was a second for the same turn, which says nothing new. */
|
|
567
613
|
terminal(frame) {
|
|
568
614
|
if (frame.turn === this.finished)
|
package/dist/events.d.ts
CHANGED
|
@@ -102,6 +102,19 @@ export interface ToolEndFrame extends Frame<"tool.end">, InTurn {
|
|
|
102
102
|
ok: boolean;
|
|
103
103
|
ms: number;
|
|
104
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* The model called one of your app's tools (`ConnectOptions.tools`): the
|
|
107
|
+
* conversation runs its handler and answers `tool.result` within
|
|
108
|
+
* `timeout_ms`. An `action_only` call is not waited for — the reply goes
|
|
109
|
+
* on at once.
|
|
110
|
+
*/
|
|
111
|
+
export interface ToolCallFrame extends Frame<"tool.call">, InTurn {
|
|
112
|
+
call_id: string;
|
|
113
|
+
name: string;
|
|
114
|
+
arguments: Record<string, unknown>;
|
|
115
|
+
action_only: boolean;
|
|
116
|
+
timeout_ms: number;
|
|
117
|
+
}
|
|
105
118
|
/**
|
|
106
119
|
* A tool marked "ask first" waits for the person's yes: the agent asks in
|
|
107
120
|
* its own words, and `confirmTool(call_id, yes)` answers with a tap.
|
|
@@ -244,6 +257,7 @@ export type ServerFrames = Exhaustive<{
|
|
|
244
257
|
"thought.delta": ThoughtDeltaFrame;
|
|
245
258
|
"tool.start": ToolStartFrame;
|
|
246
259
|
"tool.end": ToolEndFrame;
|
|
260
|
+
"tool.call": ToolCallFrame;
|
|
247
261
|
"tool.confirm": ToolConfirmFrame;
|
|
248
262
|
knowledge: KnowledgeFrame;
|
|
249
263
|
"audio.start": AudioStartFrame;
|
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export type { AgentAudio } from "./audio.js";
|
|
11
11
|
export { type Client, type ClientOptions, createClient, DEFAULT_BASE_URL, type Facts, type RelationListParams, type Relations, type SessionGetParams, type SessionListParams, type Sessions, } from "./client.js";
|
|
12
|
-
export { type CloseInfo, type ConnectOptions, Conversation, type ConversationHandlers, type ConversationState, type Disconnect, type Dropped, READY_DEADLINE_MS, type Snapshot, type StartAudioOptions, type TranscriptOutcome, type TurnOptions, type TurnOutcome, type TurnSnapshot, } from "./conversation.js";
|
|
12
|
+
export { type CloseInfo, type ConnectOptions, Conversation, type ConversationHandlers, type ConversationState, type Disconnect, type Dropped, READY_DEADLINE_MS, type Snapshot, type StartAudioOptions, type ToolHandler, type TranscriptOutcome, type TurnOptions, type TurnOutcome, type TurnSnapshot, } from "./conversation.js";
|
|
13
13
|
export { AuthenticationError, ConflictError, ConnectionError, InsufficientCreditsError, NotFoundError, PermissionError, PlatformError, type PlatformErrorInit, RateLimitError, ServerError, TimeoutError, ValidationError, } from "./errors.js";
|
|
14
14
|
export type * from "./events.js";
|
|
15
15
|
export type * from "./gen/index.js";
|
package/dist/protocol.gen.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/** The protocol, sent as the WebSocket subprotocol and echoed on `ready`. */
|
|
2
2
|
export declare const PROTOCOL = "ino.v2";
|
|
3
3
|
/** Every frame a client may send; the edge closes on any other (4009). */
|
|
4
|
-
export declare const CLIENT_VERBS: readonly ["auth", "user_message", "user_audio.start", "user_audio.end", "cancel", "mute", "ping", "tool.confirmed"];
|
|
4
|
+
export declare const CLIENT_VERBS: readonly ["auth", "user_message", "user_audio.start", "user_audio.end", "cancel", "mute", "ping", "tool.confirmed", "tool.result"];
|
|
5
5
|
export type ClientVerb = (typeof CLIENT_VERBS)[number];
|
|
6
6
|
/** Every event the edge sends. */
|
|
7
|
-
export declare const SERVER_EVENTS: readonly ["ready", "turn.start", "heard", "heard.partial", "transcript", "mood", "text.delta", "text.chunk", "thought.delta", "tool.start", "tool.end", "tool.confirm", "knowledge", "audio.start", "audio.end", "muted", "relation", "usage", "turn.done", "interrupted", "error", "pong"];
|
|
7
|
+
export declare const SERVER_EVENTS: readonly ["ready", "turn.start", "heard", "heard.partial", "transcript", "mood", "text.delta", "text.chunk", "thought.delta", "tool.start", "tool.end", "tool.call", "tool.confirm", "knowledge", "audio.start", "audio.end", "muted", "relation", "usage", "turn.done", "interrupted", "error", "pong"];
|
|
8
8
|
export type ServerEventName = (typeof SERVER_EVENTS)[number];
|
|
9
9
|
/** The events that end a turn: exactly one of them ends every turn. */
|
|
10
10
|
export declare const TERMINAL_EVENTS: readonly ["turn.done", "interrupted"];
|
|
@@ -45,3 +45,5 @@ export declare const AUDIO_CODEC = "pcm_s16le";
|
|
|
45
45
|
export declare const AUDIO_CHANNELS = 1;
|
|
46
46
|
export declare const DEFAULT_SAMPLE_RATE = 24000;
|
|
47
47
|
export declare const MAX_SOCKETS_PER_END_USER = 3;
|
|
48
|
+
export declare const APP_TOOLS_MAX = 50;
|
|
49
|
+
export declare const TOOL_ERROR_MAX_CHARS = 2000;
|
package/dist/protocol.gen.js
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
/** The protocol, sent as the WebSocket subprotocol and echoed on `ready`. */
|
|
6
6
|
export const PROTOCOL = "ino.v2";
|
|
7
7
|
/** Every frame a client may send; the edge closes on any other (4009). */
|
|
8
|
-
export const CLIENT_VERBS = ["auth", "user_message", "user_audio.start", "user_audio.end", "cancel", "mute", "ping", "tool.confirmed"];
|
|
8
|
+
export const CLIENT_VERBS = ["auth", "user_message", "user_audio.start", "user_audio.end", "cancel", "mute", "ping", "tool.confirmed", "tool.result"];
|
|
9
9
|
/** Every event the edge sends. */
|
|
10
|
-
export const SERVER_EVENTS = ["ready", "turn.start", "heard", "heard.partial", "transcript", "mood", "text.delta", "text.chunk", "thought.delta", "tool.start", "tool.end", "tool.confirm", "knowledge", "audio.start", "audio.end", "muted", "relation", "usage", "turn.done", "interrupted", "error", "pong"];
|
|
10
|
+
export const SERVER_EVENTS = ["ready", "turn.start", "heard", "heard.partial", "transcript", "mood", "text.delta", "text.chunk", "thought.delta", "tool.start", "tool.end", "tool.call", "tool.confirm", "knowledge", "audio.start", "audio.end", "muted", "relation", "usage", "turn.done", "interrupted", "error", "pong"];
|
|
11
11
|
/** The events that end a turn: exactly one of them ends every turn. */
|
|
12
12
|
export const TERMINAL_EVENTS = ["turn.done", "interrupted"];
|
|
13
13
|
/** A `ping` of exactly this text is answered by the edge's runtime with `KEEPALIVE_PONG`, which has no `seq`. */
|
|
@@ -47,3 +47,5 @@ export const AUDIO_CODEC = "pcm_s16le";
|
|
|
47
47
|
export const AUDIO_CHANNELS = 1;
|
|
48
48
|
export const DEFAULT_SAMPLE_RATE = 24000;
|
|
49
49
|
export const MAX_SOCKETS_PER_END_USER = 3;
|
|
50
|
+
export const APP_TOOLS_MAX = 50;
|
|
51
|
+
export const TOOL_ERROR_MAX_CHARS = 2000;
|
package/dist/socket.d.ts
CHANGED
|
@@ -81,6 +81,8 @@ export interface DialOptions {
|
|
|
81
81
|
url: string;
|
|
82
82
|
key: string;
|
|
83
83
|
sessionId: string;
|
|
84
|
+
/** The app tools this connection runs, by name: `auth.tools`. */
|
|
85
|
+
tools?: string[];
|
|
84
86
|
WebSocket: WebSocketConstructor;
|
|
85
87
|
onFrame(frame: ServerFrame): void;
|
|
86
88
|
onAudio(frame: AudioFrame): void;
|
package/dist/socket.js
CHANGED
|
@@ -21,11 +21,13 @@ export function dial(options) {
|
|
|
21
21
|
return true;
|
|
22
22
|
};
|
|
23
23
|
socket.onopen = () => {
|
|
24
|
-
//
|
|
24
|
+
// The edge refuses a field it does not know by name, so `tools` goes
|
|
25
|
+
// only when there are some: no app tools, no field.
|
|
25
26
|
send(JSON.stringify({
|
|
26
27
|
type: "auth",
|
|
27
28
|
key: options.key,
|
|
28
29
|
session_id: options.sessionId,
|
|
30
|
+
...(options.tools?.length ? { tools: options.tools } : {}),
|
|
29
31
|
}));
|
|
30
32
|
};
|
|
31
33
|
socket.onmessage = ({ data }) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lumavox/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Talk to Lumavox agents from a browser, a React Native app or Node, with an end user's ephemeral key: conversations on the agent's socket, sessions, memory and relations. Zero dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|