@telnyx/ai-agent-lib 0.4.5 → 0.5.0-beta.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
@@ -184,6 +184,9 @@ Returns the `TelnyxAIAgent` instance for direct API access.
184
184
  - `startConversation(options?)` - Start a new conversation with optional caller metadata and headers
185
185
  - `endConversation()` - End the current conversation
186
186
  - `sendConversationMessage(message: string)` - Send a text message during an active conversation
187
+ - `registerClientTool(name, handler)` - Register a client-side tool the assistant can invoke (see [Client-Side Tools](#client-side-tools))
188
+ - `unregisterClientTool(name)` - Remove a registered client-side tool
189
+ - `getClientTools()` - List registered client-side tool names
187
190
  - `setRemoteStream(stream: MediaStream)` - Manually set the remote audio stream for monitoring (useful when `call.remoteStream` is not available)
188
191
  - `transcript` - Get current transcript array
189
192
 
@@ -342,6 +345,121 @@ agent.on('conversation.update', (notification) => {
342
345
  - The agent will receive and process text messages just like spoken input
343
346
  - Text messages may appear in the transcript depending on the agent configuration
344
347
 
348
+ ### Client-Side Tools
349
+
350
+ Client-side tools let the AI assistant call functions that run **in the
351
+ browser / client** during a conversation. The assistant decides when to call a
352
+ tool, the library executes your handler, and the return value is sent back to
353
+ the assistant so it can continue the conversation. This implements the ACA
354
+ `client_side_tool` execution path over the Voice SDK Proxy (VSP) WebSocket.
355
+
356
+ Typical uses: looking up data the page already has, reading client-side state,
357
+ triggering UI actions, or calling an API the browser is authenticated for.
358
+
359
+ #### Registering tools
360
+
361
+ Register tools up front via the constructor, or at any time with
362
+ `registerClientTool`:
363
+
364
+ ```typescript
365
+ import { TelnyxAIAgent } from '@telnyx/ai-agent-lib';
366
+
367
+ const agent = new TelnyxAIAgent({
368
+ agentId: 'your-agent-id',
369
+ // Register at construction time:
370
+ clientTools: {
371
+ lookup_order: async (args, context) => {
372
+ // args is the parsed `arguments` object from the assistant
373
+ const order = await fetchOrder(args.orderId);
374
+ return { status: 'found', orderId: order.id, total: order.total };
375
+ },
376
+ },
377
+ // Optional: per-tool execution timeout (default 30000ms)
378
+ clientToolTimeoutMs: 15000,
379
+ });
380
+
381
+ // ...or register/replace later:
382
+ agent.registerClientTool('get_cart', () => ({ items: cart.items.length }));
383
+
384
+ // Remove a tool:
385
+ agent.unregisterClientTool('get_cart');
386
+
387
+ // Inspect registered tools:
388
+ agent.getClientTools(); // ['lookup_order']
389
+ ```
390
+
391
+ In React you can do the same via `useClient()`:
392
+
393
+ ```tsx
394
+ function ToolRegistration() {
395
+ const client = useClient();
396
+ useEffect(() => {
397
+ client.registerClientTool('lookup_order', async (args) => {
398
+ return { status: 'found', orderId: args.orderId };
399
+ });
400
+ return () => client.unregisterClientTool('lookup_order');
401
+ }, [client]);
402
+ return null;
403
+ }
404
+ ```
405
+
406
+ #### Handler contract
407
+
408
+ ```typescript
409
+ type ClientSideToolHandler = (
410
+ args: unknown, // parsed JSON arguments (or undefined if empty)
411
+ context: ClientSideToolContext, // { callId, toolName, rawArguments }
412
+ ) => unknown | Promise<unknown>;
413
+ ```
414
+
415
+ - The return value is serialized and sent back as the tool output. Strings are
416
+ sent verbatim; anything else is `JSON.stringify`-ed.
417
+ - Handlers may be async. They are run with a timeout (`clientToolTimeoutMs`).
418
+ - The tool name and `call_id` must round-trip back to the assistant — the
419
+ library handles that for you.
420
+
421
+ #### Safety & robustness
422
+
423
+ The library always returns a result to the assistant so the conversation never
424
+ hangs, and it never executes a tool twice for the same call:
425
+
426
+ - **Unknown tool** → safe error output `{ "error": "unknown_tool" }`.
427
+ - **Invalid JSON arguments** → `{ "error": "invalid_arguments" }`.
428
+ - **Handler throws / rejects** → `{ "error": "handler_error" }`.
429
+ - **Handler exceeds timeout** → `{ "error": "timeout" }`.
430
+ - **Disconnected before output can be sent** → `{ "error": "shutdown" }` event,
431
+ output dropped (ACA has already timed out its waiter).
432
+ - **Duplicate `call_id`** while a tool is in-flight → ignored (no double run).
433
+ - On disconnect, in-flight bookkeeping is cleared so reconnects start clean.
434
+
435
+ > Tool arguments and outputs are **never logged** — they may contain customer
436
+ > data. Only safe correlation fields (`call_id`, tool name) appear in debug logs.
437
+
438
+ #### Observing tool lifecycle
439
+
440
+ Three events let you observe tool execution without touching payloads:
441
+
442
+ ```typescript
443
+ agent.on('client.tool.invoked', ({ callId, toolName }) => {
444
+ console.log(`tool ${toolName} invoked (${callId})`);
445
+ });
446
+ agent.on('client.tool.completed', ({ callId, toolName, isError }) => {
447
+ console.log(`tool ${toolName} completed, isError=${isError}`);
448
+ });
449
+ agent.on('client.tool.error', ({ callId, toolName, reason }) => {
450
+ console.warn(`tool ${toolName} failed: ${reason}`);
451
+ });
452
+ ```
453
+
454
+ #### EVA adapter usage
455
+
456
+ The EVA adapter (`telnyx-voice-ai-eva-adapter`) is expected to consume this same
457
+ public API — registering its EVA-specific tools through `registerClientTool`
458
+ (or the `clientTools` constructor option) rather than reimplementing the
459
+ PR-531 protocol. The core client-side tool support lives here and in
460
+ `@telnyx/webrtc`, so it works for any VSP-connected Voice SDK client
461
+ independently of the EVA adapter.
462
+
345
463
  ### Latency Measurement
346
464
 
347
465
  The library automatically measures round-trip latency using client-side Voice Activity Detection (VAD). This provides accurate timing from when the user stops speaking until the agent's response audio begins.
@@ -498,6 +616,9 @@ The `TelnyxAIAgent` class extends `EventEmitter` and provides a comprehensive se
498
616
  | `conversation.update` | `INotification` | Emitted when conversation state changes |
499
617
  | `conversation.agent.state` | `AgentStateData` | Emitted when agent state changes (listening/speaking/thinking) |
500
618
  | `agent.audio.mute` | `boolean` | Emitted when agent audio is muted or unmuted |
619
+ | `client.tool.invoked` | `{ callId, toolName }` | Emitted when a client-side tool starts executing |
620
+ | `client.tool.completed` | `{ callId, toolName, isError }` | Emitted when a client-side tool output is sent back to the assistant |
621
+ | `client.tool.error` | `{ callId, toolName, reason }` | Emitted when a client-side tool fails and a safe error output is sent |
501
622
 
502
623
  ### Data Types
503
624
 
@@ -0,0 +1,112 @@
1
+ import type { Call } from "@telnyx/webrtc";
2
+ import type { ClientSideToolErrorReason, ClientSideToolHandler, ClientSideToolMap } from "./types";
3
+ /** Default per-tool execution timeout (ms). */
4
+ export declare const DEFAULT_CLIENT_TOOL_TIMEOUT_MS = 30000;
5
+ type ToolLifecycleEvents = {
6
+ onInvoked: (info: {
7
+ callId: string;
8
+ toolName: string;
9
+ }) => void;
10
+ onCompleted: (info: {
11
+ callId: string;
12
+ toolName: string;
13
+ isError: boolean;
14
+ }) => void;
15
+ onError: (info: {
16
+ callId: string;
17
+ toolName: string;
18
+ reason: ClientSideToolErrorReason;
19
+ }) => void;
20
+ };
21
+ type ClientToolManagerOptions = {
22
+ /**
23
+ * Returns the currently active {@link Call}, or `null` when no call is
24
+ * connected. The manager uses it to send `function_call_output` back to ACA.
25
+ */
26
+ getActiveCall: () => Call | null;
27
+ /** Per-tool execution timeout in milliseconds. */
28
+ timeoutMs?: number;
29
+ /** Lifecycle event hooks (wired to the TelnyxAIAgent EventEmitter). */
30
+ events: ToolLifecycleEvents;
31
+ };
32
+ /**
33
+ * Manages registration and PR-531 execution of client-side tools.
34
+ *
35
+ * Responsibilities:
36
+ * - hold the tool registry
37
+ * - subscribe to inbound `function_call` items (via {@link handleEvent})
38
+ * - parse `arguments`, run the matching handler with a timeout
39
+ * - de-duplicate concurrent AND already-completed invocations sharing a
40
+ * `call_id` (idempotency across VSP/ACA re-delivery)
41
+ * - always send a `function_call_output` (result or safe error) back to ACA
42
+ * - clean up in-flight/completed bookkeeping on disconnect, and drop late
43
+ * handler resolutions that belong to a previous session generation
44
+ *
45
+ * Tool arguments and outputs are NEVER logged (they may contain customer data).
46
+ */
47
+ export declare class ClientToolManager {
48
+ private readonly registry;
49
+ private readonly inFlight;
50
+ /**
51
+ * `call_id`s that have already produced a `function_call_output` in the
52
+ * current session. Retained (until {@link reset}) so a re-delivered
53
+ * `function_call` for an already-handled id is ignored instead of re-running
54
+ * a handler with side effects.
55
+ */
56
+ private readonly completed;
57
+ /**
58
+ * Monotonic session generation. Bumped on every {@link reset}/{@link destroy}
59
+ * so a handler that resolves after a disconnect can detect that its session
60
+ * is gone and avoid sending a stale output against a reconnected call.
61
+ */
62
+ private generation;
63
+ private readonly getActiveCall;
64
+ private readonly timeoutMs;
65
+ private readonly events;
66
+ constructor(options: ClientToolManagerOptions);
67
+ /** Registers (or replaces) a handler for `name`. */
68
+ register(name: string, handler: ClientSideToolHandler): void;
69
+ /** Bulk-registers handlers from a map (used by the constructor option). */
70
+ registerAll(tools: ClientSideToolMap): void;
71
+ /** Removes a handler. Returns true when a handler was removed. */
72
+ unregister(name: string): boolean;
73
+ /** True when a handler is registered for `name`. */
74
+ has(name: string): boolean;
75
+ /** Registered tool names. */
76
+ list(): string[];
77
+ /**
78
+ * Inbound `telnyx.ai.conversation` event handler. Ignores anything that is
79
+ * not a PR-531 `function_call` so transcript / state messages are untouched.
80
+ */
81
+ handleEvent: (event: unknown) => void;
82
+ /**
83
+ * Drops all in-flight/completed bookkeeping and advances the session
84
+ * generation. Called on disconnect so a later reconnect starts clean, and so
85
+ * a handler still running from the old session drops its (now stale) output
86
+ * instead of sending it against the new call.
87
+ */
88
+ reset(): void;
89
+ /** Clears registry and all session state. Called on full teardown. */
90
+ destroy(): void;
91
+ /**
92
+ * Records a terminal `call_id` for idempotency, but only if the session has
93
+ * not been reset since the invocation started — otherwise the id belongs to a
94
+ * dead generation and must not leak into the next session's dedupe set.
95
+ */
96
+ private markCompleted;
97
+ private execute;
98
+ private withTimeout;
99
+ /**
100
+ * Sends a `function_call_output` back to ACA via the active call. When the
101
+ * session is gone (disconnected / hung up) the output is dropped with a
102
+ * shutdown error event rather than queued — ACA has its own waiter timeout
103
+ * and would reject a stale late result anyway.
104
+ *
105
+ * If the session generation has advanced since the invocation started (a
106
+ * reset/disconnect happened while the handler was running), the result is
107
+ * considered stale and is dropped — sending it would leak the old result
108
+ * into a freshly reconnected call/session.
109
+ */
110
+ private sendOutput;
111
+ }
112
+ export {};
package/dist/client.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Call } from "@telnyx/webrtc";
2
2
  import EventEmitter from "eventemitter3";
3
- import type { AIAgentEvents, TranscriptItem, VADOptions } from "./types";
3
+ import type { AIAgentEvents, ClientSideToolHandler, ClientSideToolMap, TranscriptItem, VADOptions } from "./types";
4
4
  export type TelnyxAIAgentConstructorParams = {
5
5
  agentId: string;
6
6
  versionId?: string;
@@ -35,6 +35,27 @@ export type TelnyxAIAgentConstructorParams = {
35
35
  * @default true
36
36
  */
37
37
  skipLastVoiceSdkId?: boolean;
38
+ /**
39
+ * Client-side tools the AI assistant can invoke during a conversation
40
+ * (PR-531 `client_side_tool` execution). Each entry maps a tool name to a
41
+ * handler whose return value is sent back to the assistant as the tool
42
+ * output. Tools can also be added later with {@link TelnyxAIAgent.registerClientTool}.
43
+ *
44
+ * @example
45
+ * new TelnyxAIAgent({
46
+ * agentId,
47
+ * clientTools: {
48
+ * lookup_order: async (args) => ({ status: "found", orderId: args.orderId }),
49
+ * },
50
+ * });
51
+ */
52
+ clientTools?: ClientSideToolMap;
53
+ /**
54
+ * Per-tool execution timeout in milliseconds for client-side tools. After
55
+ * this elapses the handler result is abandoned and a safe timeout error is
56
+ * returned to the assistant. Defaults to 30000ms.
57
+ */
58
+ clientToolTimeoutMs?: number;
38
59
  };
39
60
  export declare class TelnyxAIAgent extends EventEmitter<AIAgentEvents> {
40
61
  private telnyxRTC;
@@ -48,6 +69,7 @@ export declare class TelnyxAIAgent extends EventEmitter<AIAgentEvents> {
48
69
  conversationId?: string;
49
70
  debug: boolean;
50
71
  private audioStreamMonitor;
72
+ private clientTools;
51
73
  activeCall: Call | null;
52
74
  /**
53
75
  * When true, the client operates in chat-only mode (no microphone).
@@ -75,6 +97,30 @@ export declare class TelnyxAIAgent extends EventEmitter<AIAgentEvents> {
75
97
  */
76
98
  callReportId: string | null;
77
99
  constructor(params: TelnyxAIAgentConstructorParams);
100
+ /**
101
+ * Registers (or replaces) a client-side tool the AI assistant can invoke.
102
+ *
103
+ * The handler receives the parsed `arguments` object and an invocation
104
+ * context, and its return value is serialized and sent back to the assistant.
105
+ * Throwing / rejecting is caught and reported to the assistant as a safe
106
+ * error output so the conversation continues.
107
+ *
108
+ * @example
109
+ * agent.registerClientTool("lookup_order", async (args) => {
110
+ * return { status: "found", orderId: args.orderId };
111
+ * });
112
+ */
113
+ registerClientTool(name: string, handler: ClientSideToolHandler): void;
114
+ /**
115
+ * Removes a previously-registered client-side tool.
116
+ *
117
+ * @returns true if a handler was removed, false if none was registered.
118
+ */
119
+ unregisterClientTool(name: string): boolean;
120
+ /**
121
+ * Returns the names of all currently-registered client-side tools.
122
+ */
123
+ getClientTools(): string[];
78
124
  /**
79
125
  * Connects to the Telnyx WebRTC service and establishes a session with the AI agent.
80
126
  *