@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 +121 -0
- package/dist/client-tools.d.ts +112 -0
- package/dist/client.d.ts +47 -1
- package/dist/index.js +2381 -1998
- package/dist/message.d.ts +17 -0
- package/dist/types.d.ts +52 -0
- package/package.json +6 -3
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
|
*
|