@lumavox/client 0.4.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 CHANGED
@@ -89,7 +89,7 @@ key, dials, authenticates, and from then on:
89
89
 
90
90
  | Handler | Frame |
91
91
  |---|---|
92
- | `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 |
92
+ | `onReady` `onTurnStart` `onHeard` `onHeardPartial` `onKnowledge` `onMood` `onThought` `onText` `onChunk` `onToolStart` `onToolEnd` `onToolConfirm` `onUsage` `onMuted` `onAudioStart` `onAudioEnd` `onTranscript` `onDone` `onInterrupted` `onError` | the wire's frame, as it is |
93
93
  | `onAudio` | `{ pcm, chunk, turn, sample_rate, channels }`: raw 16-bit PCM, never copied |
94
94
  | `onRelation` | the relationship, each change once |
95
95
  | `onState` | `connecting` `ready` `busy` `reconnecting` `dormant` `closed` |
@@ -101,6 +101,21 @@ key, dials, authenticates, and from then on:
101
101
  With no `onAudio` handler every turn asks for no speech, so nothing pays
102
102
  for audio nobody hears; `connect`'s `{ speech }` overrides that.
103
103
 
104
+ A tool marked "ask first" sends `tool.confirm` and does not run:
105
+ `confirmTool(callId, yes)` answers with the person's tap — a turn of
106
+ theirs, queued as `send` is, which runs the tool first on a yes.
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
+
104
119
  ### Speaking
105
120
 
106
121
  ```ts
@@ -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";
@@ -142,6 +142,8 @@ export interface ConversationHandlers {
142
142
  onChunk?: On<"text.chunk">;
143
143
  onToolStart?: On<"tool.start">;
144
144
  onToolEnd?: On<"tool.end">;
145
+ /** A tool that asks first waits for the person: answer with `confirmTool`. */
146
+ onToolConfirm?: On<"tool.confirm">;
145
147
  onUsage?: On<"usage">;
146
148
  onMuted?: On<"muted">;
147
149
  onAudioStart?: On<"audio.start">;
@@ -167,6 +169,12 @@ export interface ConversationHandlers {
167
169
  /** Every frame, the ones above included: what a newer edge sends that this SDK does not know. */
168
170
  onEvent?: (frame: ServerFrame) => void;
169
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;
170
178
  export interface ConnectOptions {
171
179
  /**
172
180
  * Every turn's `speech`. By default `false` when there is no `onAudio`
@@ -176,6 +184,12 @@ export interface ConnectOptions {
176
184
  speech?: boolean;
177
185
  /** Ask every turn for thinking summaries (`onThought`). */
178
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>;
179
193
  }
180
194
  /** Per turn: overrides `ConnectOptions`. */
181
195
  export interface TurnOptions {
@@ -250,6 +264,12 @@ export declare class Conversation {
250
264
  * 1 to 32,000 characters, or it is `dropped: refused` here.
251
265
  */
252
266
  send(text: string, options?: TurnOptions): Promise<TurnOutcome>;
267
+ /**
268
+ * Answer a `tool.confirm` with the person's tap: a turn of theirs — "Yes."
269
+ * or "No." in the transcript — which runs the tool first on a yes.
270
+ * Queued as `send` is; never rejects.
271
+ */
272
+ confirmTool(callId: string, yes: boolean, options?: TurnOptions): Promise<TurnOutcome>;
253
273
  /**
254
274
  * Open a recording at `rate`, which must be one of `RECORDING_RATES`.
255
275
  * Answers its id. A recording already open is abandoned, as the edge
@@ -300,6 +320,15 @@ export declare class Conversation {
300
320
  private stopTimers;
301
321
  private frame;
302
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;
303
332
  /** The end of a turn: answers whether it was a second for the same turn, which says nothing new. */
304
333
  private terminal;
305
334
  private error;
@@ -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;
@@ -26,6 +26,7 @@ const HANDLER = {
26
26
  "text.chunk": "onChunk",
27
27
  "tool.start": "onToolStart",
28
28
  "tool.end": "onToolEnd",
29
+ "tool.confirm": "onToolConfirm",
29
30
  usage: "onUsage",
30
31
  muted: "onMuted",
31
32
  "audio.start": "onAudioStart",
@@ -138,6 +139,14 @@ export class Conversation {
138
139
  }
139
140
  return this.enqueue({ type: "user_message", text }, options);
140
141
  }
142
+ /**
143
+ * Answer a `tool.confirm` with the person's tap: a turn of theirs — "Yes."
144
+ * or "No." in the transcript — which runs the tool first on a yes.
145
+ * Queued as `send` is; never rejects.
146
+ */
147
+ confirmTool(callId, yes, options = {}) {
148
+ return this.enqueue({ type: "tool.confirmed", call_id: callId, yes }, options);
149
+ }
141
150
  /**
142
151
  * Open a recording at `rate`, which must be one of `RECORDING_RATES`.
143
152
  * Answers its id. A recording already open is abandoned, as the edge
@@ -306,6 +315,7 @@ export class Conversation {
306
315
  url: socketUrl(this.setup.socketUrl, this.sessionId),
307
316
  key,
308
317
  sessionId: this.sessionId,
318
+ tools: Object.keys(this.setup.options.tools ?? {}),
309
319
  WebSocket: this.setup.WebSocket,
310
320
  onFrame: (frame) => this.frame(frame),
311
321
  onAudio: (frame) => {
@@ -499,6 +509,9 @@ export class Conversation {
499
509
  case "tool.end":
500
510
  this.update({ tool: null });
501
511
  break;
512
+ case "tool.call":
513
+ this.runTool(frame);
514
+ break;
502
515
  case "knowledge":
503
516
  this.update({
504
517
  knowledge: [...(this.snap.turn?.knowledge ?? []), frame],
@@ -554,6 +567,48 @@ export class Conversation {
554
567
  }
555
568
  this.sync();
556
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
+ }
557
612
  /** The end of a turn: answers whether it was a second for the same turn, which says nothing new. */
558
613
  terminal(frame) {
559
614
  if (frame.turn === this.finished)
@@ -686,8 +741,9 @@ export class Conversation {
686
741
  next.resolve(dropped(recording.dropped ?? "closed"));
687
742
  continue;
688
743
  }
689
- // A message abandons an open recording, at the edge and here.
690
- if (next.frame.type === "user_message" && this.open) {
744
+ // A message or a tap — a turn with no recording of its own — abandons
745
+ // an open recording, at the edge and here.
746
+ if (!recording && this.open) {
691
747
  this.abandon("cancelled");
692
748
  }
693
749
  this.inFlight = next;
package/dist/events.d.ts CHANGED
@@ -85,11 +85,59 @@ export interface TextChunkFrame extends Frame<"text.chunk">, InTurn {
85
85
  export interface ThoughtDeltaFrame extends Frame<"thought.delta">, InTurn {
86
86
  text: string;
87
87
  }
88
+ /** Who runs a tool: the platform's own, your server's, or your app's. */
89
+ export type ToolKind = "platform" | "webhook" | "app";
90
+ /** The model called a tool; its `tool.end` follows. */
88
91
  export interface ToolStartFrame extends Frame<"tool.start">, InTurn {
89
92
  name: string;
93
+ kind: ToolKind;
94
+ /** Your tool's call, `call_…`; `null` for the platform's own. */
95
+ call_id: string | null;
90
96
  }
97
+ /** A tool's call is over: whether it answered, and how long it took. */
91
98
  export interface ToolEndFrame extends Frame<"tool.end">, InTurn {
92
99
  name: string;
100
+ kind: ToolKind;
101
+ call_id: string | null;
102
+ ok: boolean;
103
+ ms: number;
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
+ }
118
+ /**
119
+ * A tool marked "ask first" waits for the person's yes: the agent asks in
120
+ * its own words, and `confirmTool(call_id, yes)` answers with a tap.
121
+ */
122
+ export interface ToolConfirmFrame extends Frame<"tool.confirm">, InTurn {
123
+ call_id: string;
124
+ name: string;
125
+ arguments: Record<string, unknown>;
126
+ }
127
+ /** One call of your tool in a turn, as `turn.done` and the transcript keep it. */
128
+ export interface ToolCall {
129
+ call_id: string;
130
+ name: string;
131
+ kind: "webhook" | "app";
132
+ arguments: Record<string, unknown>;
133
+ /** What it answered, as the model read it (16 KB at most); `null` for none. */
134
+ result: unknown;
135
+ ok: boolean;
136
+ /** What failed, as the model was told; `null` when nothing did. */
137
+ error: string | null;
138
+ ms: number;
139
+ /** Which of the reply's rounds, 1 to 3; 0 for a call the person confirmed with a tap. */
140
+ round: number;
93
141
  }
94
142
  export interface KnowledgeEntry {
95
143
  entry_id: string;
@@ -157,6 +205,8 @@ export interface TurnDoneFrame extends Frame<"turn.done">, InTurn {
157
205
  chunks_spoken: number;
158
206
  heard: string | null;
159
207
  heard_seconds: number | null;
208
+ /** Your tools it called, in order. */
209
+ tool_calls: ToolCall[];
160
210
  timings: TurnTimings;
161
211
  }
162
212
  /** The turn ended early; what was written before it stopped still stands. */
@@ -207,6 +257,8 @@ export type ServerFrames = Exhaustive<{
207
257
  "thought.delta": ThoughtDeltaFrame;
208
258
  "tool.start": ToolStartFrame;
209
259
  "tool.end": ToolEndFrame;
260
+ "tool.call": ToolCallFrame;
261
+ "tool.confirm": ToolConfirmFrame;
210
262
  knowledge: KnowledgeFrame;
211
263
  "audio.start": AudioStartFrame;
212
264
  "audio.end": AudioEndFrame;
@@ -1 +1 @@
1
- export type { ClientOptions, CompactionInfo, CompactRequest, CompactSessionData, CompactSessionError, CompactSessionErrors, CompactSessionResponse, CompactSessionResponses, CreateSessionData, CreateSessionError, CreateSessionErrors, CreateSessionResponse, CreateSessionResponses, DeleteAllFactsData, DeleteAllFactsError, DeleteAllFactsErrors, DeleteAllFactsResponse, DeleteAllFactsResponses, DeleteFactData, DeleteFactError, DeleteFactErrors, DeleteFactResponse, DeleteFactResponses, DeleteSessionData, DeleteSessionError, DeleteSessionErrors, DeleteSessionResponse, DeleteSessionResponses, ErrorCode, ErrorInfo, ErrorResponse, ExtractSessionFactsData, ExtractSessionFactsError, ExtractSessionFactsErrors, ExtractSessionFactsResponse, ExtractSessionFactsResponses, FactInfo, FactPage, FactsDeleted, GetSessionData, GetSessionError, GetSessionErrors, GetSessionResponse, GetSessionResponses, KnowledgeCitation, KnowledgeLookupInfo, ListFactsData, ListFactsError, ListFactsErrors, ListFactsResponse, ListFactsResponses, ListRelationsData, ListRelationsError, ListRelationsErrors, ListRelationsResponse, ListRelationsResponses, ListSessionsData, ListSessionsError, ListSessionsErrors, ListSessionsResponse, ListSessionsResponses, MessageInfo, MessageKnowledge, MessagePage, RelationInfo, RelationPage, SessionCreateRequest, SessionDetail, SessionInfo, SessionPage, ValidationIssue } from './types.gen.js';
1
+ export type { ClientOptions, CompactionInfo, CompactRequest, CompactSessionData, CompactSessionError, CompactSessionErrors, CompactSessionResponse, CompactSessionResponses, CreateSessionData, CreateSessionError, CreateSessionErrors, CreateSessionResponse, CreateSessionResponses, DeleteAllFactsData, DeleteAllFactsError, DeleteAllFactsErrors, DeleteAllFactsResponse, DeleteAllFactsResponses, DeleteFactData, DeleteFactError, DeleteFactErrors, DeleteFactResponse, DeleteFactResponses, DeleteSessionData, DeleteSessionError, DeleteSessionErrors, DeleteSessionResponse, DeleteSessionResponses, ErrorCode, ErrorInfo, ErrorResponse, ExtractSessionFactsData, ExtractSessionFactsError, ExtractSessionFactsErrors, ExtractSessionFactsResponse, ExtractSessionFactsResponses, FactInfo, FactPage, FactsDeleted, GetSessionData, GetSessionError, GetSessionErrors, GetSessionResponse, GetSessionResponses, KnowledgeCitation, KnowledgeLookupInfo, ListFactsData, ListFactsError, ListFactsErrors, ListFactsResponse, ListFactsResponses, ListRelationsData, ListRelationsError, ListRelationsErrors, ListRelationsResponse, ListRelationsResponses, ListSessionsData, ListSessionsError, ListSessionsErrors, ListSessionsResponse, ListSessionsResponses, MessageInfo, MessageKnowledge, MessagePage, MessageToolCall, RelationInfo, RelationPage, SessionCreateRequest, SessionDetail, SessionInfo, SessionPage, ValidationIssue } from './types.gen.js';
@@ -89,6 +89,10 @@ export type MessageInfo = {
89
89
  */
90
90
  mood_intensity: number | null;
91
91
  role: 'user' | 'assistant';
92
+ /**
93
+ * On a reply: the calls of your tools it made, in order, each with its arguments and what it answered. Empty when it called none, and on the person's messages.
94
+ */
95
+ tool_calls: Array<MessageToolCall>;
92
96
  };
93
97
  export type MessageKnowledge = {
94
98
  lookups: Array<KnowledgeLookupInfo>;
@@ -114,6 +118,40 @@ export type KnowledgeCitation = {
114
118
  score: number;
115
119
  title: string;
116
120
  };
121
+ export type MessageToolCall = {
122
+ /**
123
+ * The call's id, `call_…`: what your server or app was sent.
124
+ */
125
+ call_id: string;
126
+ name: string;
127
+ kind: 'webhook' | 'app';
128
+ /**
129
+ * What the model called it with.
130
+ */
131
+ arguments: {
132
+ [key: string]: unknown;
133
+ };
134
+ /**
135
+ * What it answered, as the model read it — its JSON, or its text, cut at 16 KB. Null when it answered nothing.
136
+ */
137
+ result?: unknown;
138
+ /**
139
+ * It answered in time, with a 2xx or a result.
140
+ */
141
+ ok: boolean;
142
+ /**
143
+ * What failed, as the model was told — or, for a tool that asks first, that it waited for the person's yes. Null when nothing did.
144
+ */
145
+ error: string | null;
146
+ /**
147
+ * How long it took.
148
+ */
149
+ ms: number;
150
+ /**
151
+ * Which of the reply's rounds of calls, 1 to 3; 0 for a call the person confirmed with a tap.
152
+ */
153
+ round: number;
154
+ };
117
155
  export type RelationInfo = {
118
156
  agent_id: string;
119
157
  /**
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";
@@ -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"];
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", "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;
@@ -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"];
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", "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
- // Two fields, and the edge refuses any third by name.
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.4.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",